Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -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");
}