Initial commit

This commit is contained in:
2026-07-03 16:37:12 +07:00
commit 63b8c1ea8b
1931 changed files with 640587 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
using RobotNet10.Script.IO;
using System.Net.Sockets;
namespace RobotNet10.ScriptEngine.IO;
/// <summary>
/// Implementation of CC-Link IE connection.
/// Note: This is a basic implementation. Full CC-Link IE support may require additional libraries.
/// </summary>
public class CcLinkIeConnection : ICcLinkIeConnection
{
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private bool _disposed;
public string IpAddress { get; }
public int StationNumber { get; }
public bool IsConnected { get; private set; }
public CcLinkIeConnection(string ipAddress, int stationNumber = 1)
{
IpAddress = ipAddress;
StationNumber = stationNumber;
IsConnected = false;
}
public async Task ConnectAsync()
{
if (IsConnected)
return;
try
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(IpAddress, 5007); // Standard CC-Link IE port
_stream = _tcpClient.GetStream();
IsConnected = true;
// TODO: Implement CC-Link IE handshake protocol
// This requires implementing the CC-Link IE protocol stack
}
catch
{
Disconnect();
throw;
}
}
public Task DisconnectAsync()
{
Disconnect();
return Task.CompletedTask;
}
private void Disconnect()
{
IsConnected = false;
_stream?.Close();
_stream = null;
_tcpClient?.Close();
_tcpClient?.Dispose();
_tcpClient = null;
}
public async Task<ushort[]> ReadAsync(int address, int length)
{
EnsureConnected();
// TODO: Implement CC-Link IE read operation
// This requires implementing the CC-Link IE protocol
await Task.CompletedTask;
throw new NotImplementedException("CC-Link IE read operation is not yet fully implemented. Full CC-Link IE support requires additional protocol implementation.");
}
public async Task WriteAsync(int address, ushort[] data)
{
EnsureConnected();
// TODO: Implement CC-Link IE write operation
// This requires implementing the CC-Link IE protocol
await Task.CompletedTask;
throw new NotImplementedException("CC-Link IE write operation is not yet fully implemented. Full CC-Link IE support requires additional protocol implementation.");
}
private void EnsureConnected()
{
if (!IsConnected || _stream == null)
{
throw new InvalidOperationException("CC-Link IE connection is not connected. Call ConnectAsync() first.");
}
}
public void Dispose()
{
if (!_disposed)
{
Disconnect();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,123 @@
using System.Net.Http.Headers;
using RobotNet10.Script.IO;
namespace RobotNet10.ScriptEngine.IO;
/// <summary>
/// Implementation of HTTP connection using HttpClient.
/// </summary>
public class HttpConnection : IHttpConnection
{
private readonly HttpClient _httpClient;
private bool _disposed;
public string BaseUrl { get; }
public bool IsConnected { get; private set; }
public HttpConnection(string baseUrl, TimeSpan? timeout = null)
{
BaseUrl = baseUrl.TrimEnd('/');
_httpClient = new HttpClient
{
BaseAddress = new Uri(BaseUrl),
Timeout = timeout ?? TimeSpan.FromSeconds(30)
};
IsConnected = false;
}
public async Task ConnectAsync()
{
// HTTP doesn't require explicit connection, but we can verify connectivity
try
{
var response = await _httpClient.GetAsync("/");
IsConnected = true;
}
catch
{
IsConnected = false;
throw;
}
}
public Task DisconnectAsync()
{
IsConnected = false;
return Task.CompletedTask;
}
public async Task<string> GetAsync(string path, Dictionary<string, string>? headers = null)
{
EnsureConnected();
using var request = new HttpRequestMessage(HttpMethod.Get, path);
AddHeaders(request, headers);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<string> PostAsync(string path, string content, string contentType = "application/json", Dictionary<string, string>? headers = null)
{
EnsureConnected();
using var request = new HttpRequestMessage(HttpMethod.Post, path);
request.Content = new StringContent(content);
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
AddHeaders(request, headers);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<string> PutAsync(string path, string content, string contentType = "application/json", Dictionary<string, string>? headers = null)
{
EnsureConnected();
using var request = new HttpRequestMessage(HttpMethod.Put, path);
request.Content = new StringContent(content);
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
AddHeaders(request, headers);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<string> DeleteAsync(string path, Dictionary<string, string>? headers = null)
{
EnsureConnected();
using var request = new HttpRequestMessage(HttpMethod.Delete, path);
AddHeaders(request, headers);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
private static void AddHeaders(HttpRequestMessage request, Dictionary<string, string>? headers)
{
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
}
private void EnsureConnected()
{
if (!IsConnected)
{
throw new InvalidOperationException("HTTP connection is not connected. Call ConnectAsync() first.");
}
}
public void Dispose()
{
if (!_disposed)
{
_httpClient?.Dispose();
IsConnected = false;
_disposed = true;
}
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,134 @@
using Modbus.Device;
using RobotNet10.Script.IO;
using System.Net.Sockets;
namespace RobotNet10.ScriptEngine.IO;
/// <summary>
/// Implementation of ModbusTCP connection using NModbus4 library.
/// </summary>
public class ModbusTcpConnection(string ipAddress, int port = 502, byte slaveId = 1, int connectTimeoutMs = 5000) : IModbusTcpConnection
{
private TcpClient? _tcpClient;
private ModbusMaster? _modbusMaster;
private bool _disposed;
public string IpAddress { get; } = ipAddress;
public int Port { get; } = port;
public byte SlaveId { get; } = slaveId;
public int ConnectTimeoutMs { get; } = connectTimeoutMs;
public bool IsConnected { get; private set; } = false;
public async Task ConnectAsync()
{
if (IsConnected)
return;
try
{
_tcpClient = new TcpClient();
// Sử dụng ConnectTimeout
using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(ConnectTimeoutMs)))
{
await _tcpClient.ConnectAsync(IpAddress, Port).WaitAsync(cts.Token);
}
_modbusMaster = ModbusIpMaster.CreateIp(_tcpClient);
IsConnected = true;
}
catch (OperationCanceledException)
{
Disconnect();
throw new TimeoutException($"Connection to {IpAddress}:{Port} timed out after {ConnectTimeoutMs}ms");
}
catch
{
Disconnect();
throw;
}
}
public Task DisconnectAsync()
{
Disconnect();
return Task.CompletedTask;
}
private void Disconnect()
{
IsConnected = false;
_modbusMaster?.Dispose();
_modbusMaster = null;
_tcpClient?.Close();
_tcpClient?.Dispose();
_tcpClient = null;
}
public async Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort numberOfPoints)
{
EnsureConnected();
return await Task.Run(() => _modbusMaster!.ReadHoldingRegisters(SlaveId, startAddress, numberOfPoints));
}
public async Task<ushort[]> ReadInputRegistersAsync(ushort startAddress, ushort numberOfPoints)
{
EnsureConnected();
return await Task.Run(() => _modbusMaster!.ReadInputRegisters(SlaveId, startAddress, numberOfPoints));
}
public async Task<bool[]> ReadCoilsAsync(ushort startAddress, ushort numberOfPoints)
{
EnsureConnected();
return await Task.Run(() => _modbusMaster!.ReadCoils(SlaveId, startAddress, numberOfPoints));
}
public async Task<bool[]> ReadDiscreteInputsAsync(ushort startAddress, ushort numberOfPoints)
{
EnsureConnected();
return await Task.Run(() => _modbusMaster!.ReadInputs(SlaveId, startAddress, numberOfPoints));
}
public async Task WriteSingleCoilAsync(ushort coilAddress, bool value)
{
EnsureConnected();
await Task.Run(() => _modbusMaster!.WriteSingleCoil(SlaveId, coilAddress, value));
}
public async Task WriteMultipleCoilsAsync(ushort startAddress, bool[] values)
{
EnsureConnected();
await Task.Run(() => _modbusMaster!.WriteMultipleCoils(SlaveId, startAddress, values));
}
public async Task WriteSingleRegisterAsync(ushort registerAddress, ushort value)
{
EnsureConnected();
await Task.Run(() => _modbusMaster!.WriteSingleRegister(SlaveId, registerAddress, value));
}
public async Task WriteMultipleRegistersAsync(ushort startAddress, ushort[] values)
{
EnsureConnected();
await Task.Run(() => _modbusMaster!.WriteMultipleRegisters(SlaveId, startAddress, values));
}
private void EnsureConnected()
{
if (!IsConnected || _modbusMaster == null)
{
throw new InvalidOperationException("ModbusTCP connection is not connected. Call ConnectAsync() first.");
}
}
public void Dispose()
{
if (!_disposed)
{
Disconnect();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}

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

View File

@@ -0,0 +1,105 @@
using RobotNet10.Script.IO;
using System.Net.Sockets;
namespace RobotNet10.ScriptEngine.IO;
/// <summary>
/// Implementation of ProfiNet connection.
/// Note: This is a basic implementation. Full ProfiNet support may require additional libraries.
/// </summary>
public class ProfiNetConnection : IProfiNetConnection
{
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private bool _disposed;
public string IpAddress { get; }
public int Slot { get; }
public int Subslot { get; }
public bool IsConnected { get; private set; }
public ProfiNetConnection(string ipAddress, int slot = 1, int subslot = 1)
{
IpAddress = ipAddress;
Slot = slot;
Subslot = subslot;
IsConnected = false;
}
public async Task ConnectAsync()
{
if (IsConnected)
return;
try
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(IpAddress, 34964); // Standard ProfiNet port
_stream = _tcpClient.GetStream();
IsConnected = true;
// TODO: Implement ProfiNet DCP (Discovery and Configuration Protocol) handshake
// This requires implementing the ProfiNet protocol stack
}
catch
{
Disconnect();
throw;
}
}
public Task DisconnectAsync()
{
Disconnect();
return Task.CompletedTask;
}
private void Disconnect()
{
IsConnected = false;
_stream?.Close();
_stream = null;
_tcpClient?.Close();
_tcpClient?.Dispose();
_tcpClient = null;
}
public async Task<byte[]> ReadAsync(int index, int length)
{
EnsureConnected();
// TODO: Implement ProfiNet read operation
// This requires implementing the ProfiNet IO data exchange protocol
await Task.CompletedTask;
throw new NotImplementedException("ProfiNet read operation is not yet fully implemented. Full ProfiNet support requires additional protocol implementation.");
}
public async Task WriteAsync(int index, byte[] data)
{
EnsureConnected();
// TODO: Implement ProfiNet write operation
// This requires implementing the ProfiNet IO data exchange protocol
await Task.CompletedTask;
throw new NotImplementedException("ProfiNet write operation is not yet fully implemented. Full ProfiNet support requires additional protocol implementation.");
}
private void EnsureConnected()
{
if (!IsConnected || _stream == null)
{
throw new InvalidOperationException("ProfiNet connection is not connected. Call ConnectAsync() first.");
}
}
public void Dispose()
{
if (!_disposed)
{
Disconnect();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}