using System.Diagnostics; using Microsoft.Extensions.Logging; using Opc.Ua; using Opc.Ua.Client; using RobotNet10.Script.IO; namespace RobotNet10.ScriptEngine.IO; /// /// Implementation of OPC UA connection using OPCFoundation.NetStandard.Opc.Ua library. /// 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() : 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 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> 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(); 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 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 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(); } 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); } } /// /// Simple telemetry context implementation for OPC UA /// 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"); }