first commit -push
This commit is contained in:
330
RobotNet.ScriptManager/Clients/RobotManagerHubClient.cs
Normal file
330
RobotNet.ScriptManager/Clients/RobotManagerHubClient.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using RobotNet.Clients;
|
||||
using RobotNet.RobotShares.Models;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Expressions;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Clients;
|
||||
|
||||
public class RobotManagerHubClient(string RobotId, string hubUrl, Func<Task<string?>> accessTokenProvider) : HubClient(new Uri(hubUrl), accessTokenProvider), IRobot
|
||||
{
|
||||
private async Task<RobotStateModel> GetRobotStateModel()
|
||||
{
|
||||
var result = await Connection.InvokeAsync<MessageResult<RobotStateModel>>(nameof(GetState), RobotId);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to get robot state: {result.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result.Data is null)
|
||||
{
|
||||
throw new InvalidOperationException("Robot state data is null.");
|
||||
}
|
||||
return result.Data;
|
||||
}
|
||||
}
|
||||
|
||||
private static string RobotErrorToString(IEnumerable<RobotNet.RobotShares.VDA5050.State.Error> errors)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var error in errors)
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
sb.Append($"{error.ErrorLevel}: {error.ErrorType} {error.ErrorDescription} - {error.ErrorHint}");
|
||||
foreach (var reference in error.ErrorReferences ?? [])
|
||||
{
|
||||
sb.Append($" [{reference.ReferenceKey}: {reference.ReferenceValue}]");
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task<RobotState> GetState() => VDA5050ScriptHelper.ConvertToRobotState(await GetRobotStateModel());
|
||||
|
||||
public async Task<RobotLoad[]> GetLoads()
|
||||
{
|
||||
var stateModel = await GetRobotStateModel();
|
||||
if (stateModel.Loads is null || stateModel.Loads.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
return [.. stateModel.Loads.Select(load => new RobotLoad(load.LoadId, load.LoadType, load.LoadPosition, load.Weight))];
|
||||
}
|
||||
|
||||
public async Task<RobotResult> Execute(RobotAction action, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = await GetState();
|
||||
if (!state.IsReady)
|
||||
{
|
||||
throw new InvalidOperationException($"Robot '{RobotId}' không sẵn sàng để thực hiện action [{action.ActionType}]!");
|
||||
}
|
||||
|
||||
var model = VDA5050ScriptHelper.ConvertToRobotInstantActionModel(RobotId, action);
|
||||
var result = await Connection.InvokeAsync<MessageResult<string>>("InstantAction", model, cancellationToken);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to perform instant action: {result.Message}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(result.Data))
|
||||
{
|
||||
throw new InvalidOperationException("Instant action ID is empty.");
|
||||
}
|
||||
|
||||
bool isRunning = true;
|
||||
bool isSuccess = false;
|
||||
string message = "";
|
||||
while (isRunning)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"Action [{action.ActionType} is canceled";
|
||||
break;
|
||||
}
|
||||
|
||||
var stateModel = await GetRobotStateModel();
|
||||
var stateAction = stateModel.ActionStates.FirstOrDefault(a => a.ActionId == result.Data);
|
||||
if (stateAction is null)
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"Action [{action.ActionType}] not found in robot state.";
|
||||
}
|
||||
else if (stateAction.IsError)
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"Action [{action.ActionType}] failed with result: {stateAction.Action?.ResultDescription}, Error: {RobotErrorToString(stateModel.Errors)}";
|
||||
}
|
||||
else if (stateAction.IsProcessing)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"Action [{action.ActionType}] wait cancel";
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(50, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"Action [{action.ActionType}] wait cancel";
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (stateAction.IsCompleted)
|
||||
{
|
||||
isRunning = false;
|
||||
isSuccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"Action [{action.ActionType}] is in an unexpected state";
|
||||
}
|
||||
}
|
||||
return new(isSuccess, message);
|
||||
}
|
||||
|
||||
public async Task AbortMovement()
|
||||
{
|
||||
var result = await Connection.InvokeAsync<MessageResult>("CancelOrder", RobotId);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to cancel robot operation: {result.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<RobotResult> WaitOrder(string premessage, CancellationToken cancellationToken)
|
||||
{
|
||||
bool isRunning = true;
|
||||
bool isSuccess = false;
|
||||
string message = "";
|
||||
while (isRunning)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
message = $"{premessage} is canceled";
|
||||
break;
|
||||
}
|
||||
var stateModel = await GetRobotStateModel();
|
||||
if (stateModel.OrderState.IsError)
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"{premessage} failed with error: {string.Join("\n\t", stateModel.OrderState.Errors)} \n Robot Error: {RobotErrorToString(stateModel.Errors)}";
|
||||
}
|
||||
else if (stateModel.OrderState.IsProcessing)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"{premessage} is canceled";
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(50, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"{premessage} is canceled";
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (stateModel.OrderState.IsCompleted)
|
||||
{
|
||||
isRunning = false;
|
||||
isSuccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isRunning = false;
|
||||
message = $"{premessage} is in an unexpected state";
|
||||
}
|
||||
}
|
||||
return new(isSuccess, message);
|
||||
}
|
||||
|
||||
private async Task<RobotResult> MoveToNode(string nodeName, IDictionary<string, IEnumerable<RobotAction>> actions, double? lastAngle, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = await GetState();
|
||||
if (!state.IsReady)
|
||||
{
|
||||
throw new InvalidOperationException($"Robot '{RobotId}' không sẵn sàng để thực hiện di chuyển đến [{nodeName}]!");
|
||||
}
|
||||
|
||||
var model = VDA5050ScriptHelper.ConvertToRobotMoveToNodeModel(RobotId, nodeName, actions, lastAngle);
|
||||
|
||||
var result = await Connection.InvokeAsync<MessageResult>("MoveToNode", model, cancellationToken);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to move to node: {result.Message}");
|
||||
}
|
||||
|
||||
return await WaitOrder($"Robot move to node [{nodeName}]", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<RobotResult> Move(string nodeName, IDictionary<string, IEnumerable<RobotAction>> actions, double lastAngle, CancellationToken cancellationToken)
|
||||
=> MoveToNode(nodeName, actions, lastAngle, cancellationToken);
|
||||
|
||||
public Task<RobotResult> Move(string nodeName, IDictionary<string, IEnumerable<RobotAction>> actions, CancellationToken cancellationToken)
|
||||
=> MoveToNode(nodeName, actions, null, cancellationToken);
|
||||
|
||||
public Task<RobotResult> Move(string nodeName, IEnumerable<RobotAction> lastActions, double lastAngle, CancellationToken cancellationToken)
|
||||
=> MoveToNode(nodeName, new Dictionary<string, IEnumerable<RobotAction>> { { nodeName, lastActions } }, lastAngle, cancellationToken);
|
||||
|
||||
public Task<RobotResult> Move(string nodeName, IEnumerable<RobotAction> lastActions, CancellationToken cancellationToken)
|
||||
=> MoveToNode(nodeName, new Dictionary<string, IEnumerable<RobotAction>> { { nodeName, lastActions } }, null, cancellationToken);
|
||||
|
||||
public Task<RobotResult> Move(string nodeName, double lastAngle, CancellationToken cancellationToken)
|
||||
=> MoveToNode(nodeName, new Dictionary<string, IEnumerable<RobotAction>>(), lastAngle, cancellationToken);
|
||||
|
||||
public Task<RobotResult> Move(string nodeName, CancellationToken cancellationToken)
|
||||
=> MoveToNode(nodeName, new Dictionary<string, IEnumerable<RobotAction>>(), null, cancellationToken);
|
||||
|
||||
public async Task<RobotResult> SimMoveStraight(double x, double y, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = await GetState();
|
||||
if (!state.IsReady)
|
||||
{
|
||||
throw new InvalidOperationException($"Robot '{RobotId}' không sẵn sàng để thực hiện di chuyển đến [{x}; {y}]!");
|
||||
}
|
||||
|
||||
var result = await Connection.InvokeAsync<MessageResult>("MoveStraight", new RobotMoveStraightModel()
|
||||
{
|
||||
RobotId = RobotId,
|
||||
X = x,
|
||||
Y = y
|
||||
}, cancellationToken);
|
||||
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to move straight to [{x}; {y}]: {result.Message}");
|
||||
}
|
||||
|
||||
return await WaitOrder($"Robot move straight to [{x}; {y}]", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<RobotResult> SimRotate(double angle, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = await GetState();
|
||||
if (!state.IsReady)
|
||||
{
|
||||
throw new InvalidOperationException($"Robot '{RobotId}' không sẵn sàng để thực hiện rotate to [{angle}]!");
|
||||
}
|
||||
|
||||
var result = await Connection.InvokeAsync<MessageResult>("Rotate", new RobotRotateModel()
|
||||
{
|
||||
RobotId = RobotId,
|
||||
Angle = angle
|
||||
}, cancellationToken);
|
||||
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to rotate to [{angle}]: {result.Message}");
|
||||
}
|
||||
|
||||
return await WaitOrder($"Robot rotate to [{angle}]", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> WaitForReady(CancellationToken cancellationToken)
|
||||
{
|
||||
var state = await GetState();
|
||||
while(!state.IsReady && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(50, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return false; // Operation was canceled
|
||||
}
|
||||
state = await GetState();
|
||||
}
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return false; // Operation was canceled
|
||||
}
|
||||
return state.IsReady;
|
||||
}
|
||||
|
||||
public async Task<bool> RequestACSIn(string id)
|
||||
{
|
||||
var result = await Connection.InvokeAsync<MessageResult>(nameof(RequestACSIn), RobotId, id);
|
||||
return result.IsSuccess;
|
||||
}
|
||||
|
||||
public async Task<bool> RequestACSOut(string id)
|
||||
{
|
||||
var result = await Connection.InvokeAsync<MessageResult>(nameof(RequestACSOut), RobotId, id);
|
||||
return result.IsSuccess;
|
||||
}
|
||||
|
||||
public async Task<bool> CancelOrder()
|
||||
{
|
||||
var result = await Connection.InvokeAsync<MessageResult>(nameof(CancelOrder), RobotId);
|
||||
return result.IsSuccess;
|
||||
}
|
||||
|
||||
public async Task<bool> CancelAction()
|
||||
{
|
||||
var result = await Connection.InvokeAsync<MessageResult>(nameof(CancelOrder), RobotId);
|
||||
return result.IsSuccess;
|
||||
}
|
||||
}
|
||||
679
RobotNet.ScriptManager/Connections/CcLinkIeBasicClient.cs
Normal file
679
RobotNet.ScriptManager/Connections/CcLinkIeBasicClient.cs
Normal file
@@ -0,0 +1,679 @@
|
||||
using RobotNet.Script;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace RobotNet.ScriptManager.Connections;
|
||||
|
||||
public class CcLinkIeBasicClient(IeBasicClientOptions options) : ICcLinkIeBasicClient
|
||||
{
|
||||
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
||||
private UdpClient? _udp;
|
||||
private TcpClient? _tcp;
|
||||
private NetworkStream? _tcpStream;
|
||||
private IPEndPoint? _remoteEp;
|
||||
private CancellationTokenSource? _pollCts;
|
||||
|
||||
private IeBasicStatus _status = new()
|
||||
{
|
||||
LinkUp = false,
|
||||
ConsecutiveErrors = 0,
|
||||
LastEndCode = null,
|
||||
LastErrorText = null,
|
||||
AvgRtt = null,
|
||||
MaxRtt = null
|
||||
};
|
||||
|
||||
private volatile bool _connected;
|
||||
private long _rttCount;
|
||||
private long _rttSumTicks;
|
||||
|
||||
public bool IsConnected => _connected;
|
||||
public int RetryCount { get => options.RetryCount; set => options = options with { RetryCount = value }; }
|
||||
|
||||
public event EventHandler<PollUpdatedEventArgs>? Polled;
|
||||
|
||||
// ====== Public API ======
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (options.FrameFormat != SlmpFrameFormat.Format3E)
|
||||
throw new NotSupportedException("Triển khai tham chiếu này hiện hỗ trợ 3E binary frame.");
|
||||
|
||||
if (options.Transport == IeBasicTransport.Udp)
|
||||
{
|
||||
_udp?.Dispose();
|
||||
_udp = new UdpClient();
|
||||
_udp.Client.ReceiveTimeout = (int)options.Timeout.TotalMilliseconds;
|
||||
_udp.Client.SendTimeout = (int)options.Timeout.TotalMilliseconds;
|
||||
|
||||
var ip = await ResolveHostAsync(options.Host, ct);
|
||||
_remoteEp = new IPEndPoint(ip, options.Port);
|
||||
_udp.Connect(_remoteEp);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tcp?.Close();
|
||||
_tcp = new TcpClient
|
||||
{
|
||||
NoDelay = true,
|
||||
ReceiveTimeout = (int)options.Timeout.TotalMilliseconds,
|
||||
SendTimeout = (int)options.Timeout.TotalMilliseconds
|
||||
};
|
||||
|
||||
var ip = await ResolveHostAsync(options.Host, ct);
|
||||
await _tcp.ConnectAsync(ip, options.Port, ct);
|
||||
_tcpStream = _tcp.GetStream();
|
||||
}
|
||||
|
||||
_connected = true;
|
||||
_status = _status with { LinkUp = true, ConsecutiveErrors = 0, LastErrorText = null, LastEndCode = null };
|
||||
}
|
||||
|
||||
public Task DisconnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
StopPollingInternal();
|
||||
|
||||
_connected = false;
|
||||
|
||||
try { _tcpStream?.Dispose(); } catch { }
|
||||
try { _tcp?.Close(); } catch { }
|
||||
try { _udp?.Dispose(); } catch { }
|
||||
|
||||
_tcpStream = null;
|
||||
_tcp = null;
|
||||
_udp = null;
|
||||
|
||||
_status = _status with { LinkUp = false };
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<bool[]> ReadBitsAsync(DeviceCode device, int startAddress, int count, CancellationToken ct = default)
|
||||
{
|
||||
// 3E binary: Command 0x0401, Subcommand 0x0001 (bit units)
|
||||
var payload = BuildBatchRead(device, startAddress, count, isBit: true);
|
||||
var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
|
||||
EnsureOk(resp);
|
||||
|
||||
// Bit data: packed per bit (LSB-first) theo manual. Ở đây đơn giản dùng từng byte -> từng bit.
|
||||
return UnpackBits(resp.Payload.Span, count);
|
||||
}
|
||||
|
||||
public async Task WriteBitsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<bool> values, CancellationToken ct = default)
|
||||
{
|
||||
// 3E binary: Command 0x1401, Subcommand 0x0001 (bit units)
|
||||
var payload = BuildBatchWriteBits(device, startAddress, values.Span);
|
||||
var resp = await SendSlmpAsync(SlmpCommand.WriteDevice, payload, ct);
|
||||
EnsureOk(resp);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadWordsAsync(DeviceCode device, int startAddress, int wordCount, CancellationToken ct = default)
|
||||
{
|
||||
// 3E binary: Command 0x0401, Subcommand 0x0000 (word units)
|
||||
var payload = BuildBatchRead(device, startAddress, wordCount, isBit: false);
|
||||
var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
|
||||
EnsureOk(resp);
|
||||
|
||||
// Word data: little-endian UInt16
|
||||
if (resp.Payload.Length < wordCount * 2)
|
||||
throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
|
||||
var span = resp.Payload.Span;
|
||||
var result = new ushort[wordCount];
|
||||
for (int i = 0; i < wordCount; i++)
|
||||
result[i] = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i * 2, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task WriteWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<ushort> words, CancellationToken ct = default)
|
||||
{
|
||||
// 3E binary: Command 0x1401, Subcommand 0x0000 (word units)
|
||||
var payload = BuildBatchWriteWords(device, startAddress, words.Span);
|
||||
var resp = await SendSlmpAsync(SlmpCommand.WriteDevice, payload, ct);
|
||||
EnsureOk(resp);
|
||||
}
|
||||
|
||||
public async Task<uint[]> ReadDWordsAsync(DeviceCode device, int startAddress, int dwordCount, CancellationToken ct = default)
|
||||
{
|
||||
if (SupportsDirectDWordRead(device))
|
||||
{
|
||||
// 3E binary: Command 0x0401, Subcommand 0x0002 (dword units)
|
||||
var payload = BuildBatchRead(device, startAddress, dwordCount, isBit: false, isDWord: true);
|
||||
var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
|
||||
EnsureOk(resp);
|
||||
|
||||
if (resp.Payload.Length < dwordCount * 4)
|
||||
throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
|
||||
var span = resp.Payload.Span;
|
||||
var result = new uint[dwordCount];
|
||||
for (int i = 0; i < dwordCount; i++)
|
||||
result[i] = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(i * 4, 4));
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Đọc dword như đọc 2 word/liên tiếp
|
||||
var words = await ReadWordsAsync(device, startAddress, dwordCount * 2, ct);
|
||||
var result = new uint[dwordCount];
|
||||
|
||||
if (options.LittleEndianWordOrder)
|
||||
{
|
||||
for (int i = 0, j = 0; i < dwordCount; i++, j += 2)
|
||||
result[i] = (uint)(words[j] | ((uint)words[j + 1] << 16));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0, j = 0; i < dwordCount; i++, j += 2)
|
||||
result[i] = (uint)(words[j + 1] | ((uint)words[j] << 16));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteDWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<uint> dwords, CancellationToken ct = default)
|
||||
{
|
||||
var span = dwords.Span;
|
||||
var buf = new ushort[span.Length * 2];
|
||||
|
||||
if (options.LittleEndianWordOrder)
|
||||
{
|
||||
for (int i = 0, j = 0; i < span.Length; i++, j += 2)
|
||||
{
|
||||
buf[j] = (ushort)(span[i] & 0xFFFF);
|
||||
buf[j + 1] = (ushort)((span[i] >> 16) & 0xFFFF);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0, j = 0; i < span.Length; i++, j += 2)
|
||||
{
|
||||
buf[j] = (ushort)((span[i] >> 16) & 0xFFFF);
|
||||
buf[j + 1] = (ushort)(span[i] & 0xFFFF);
|
||||
}
|
||||
}
|
||||
await WriteWordsAsync(device, startAddress, buf, ct);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadRandomWordsAsync((DeviceCode Device, int Address)[] points, CancellationToken ct = default)
|
||||
{
|
||||
// 3E binary: Read Random (0x0403). Payload: điểm (addr+devcode)
|
||||
var payload = BuildRandomRead(points);
|
||||
var resp = await SendSlmpAsync(SlmpCommand.ReadRandom, payload, ct);
|
||||
EnsureOk(resp);
|
||||
|
||||
// Trả về word liên tiếp
|
||||
var span = resp.Payload.Span;
|
||||
if (span.Length < points.Length * 2) throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
|
||||
var result = new ushort[points.Length];
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
result[i] = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i * 2, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task WriteRandomWordsAsync((DeviceCode Device, int Address)[] points, ReadOnlyMemory<ushort> values, CancellationToken ct = default)
|
||||
{
|
||||
if (points.Length != values.Length)
|
||||
throw new ArgumentException("Số điểm và số giá trị không khớp.");
|
||||
|
||||
var payload = BuildRandomWrite(points, values.Span);
|
||||
var resp = await SendSlmpAsync(SlmpCommand.WriteRandom, payload, ct);
|
||||
EnsureOk(resp);
|
||||
}
|
||||
|
||||
public async Task<IeBasicStatus> GetStatusAsync(CancellationToken ct = default)
|
||||
{
|
||||
// Có thể gửi NodeMonitor để “làm nóng” trạng thái; ở đây trả về snapshot hiện có
|
||||
await Task.Yield();
|
||||
return _status;
|
||||
}
|
||||
|
||||
public async Task<bool> PingAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Node monitor: dùng CMD DeviceInfo/NodeMonitor rỗng để kiểm phản hồi
|
||||
var resp = await SendSlmpAsync(SlmpCommand.NodeMonitor, ReadOnlyMemory<byte>.Empty, ct);
|
||||
return resp.EndCode == SlmpEndCode.Completed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ModuleIdentity> IdentifyAsync(CancellationToken ct = default)
|
||||
{
|
||||
// Tối giản: gửi DeviceInfo và parse sơ bộ (tuỳ PLC mà payload khác nhau)
|
||||
var resp = await SendSlmpAsync(SlmpCommand.DeviceInfo, ReadOnlyMemory<byte>.Empty, ct);
|
||||
// Ở đây không chuẩn hoá do payload phụ thuộc model; trả về khung rỗng để người dùng tự giải.
|
||||
return new ModuleIdentity
|
||||
{
|
||||
Vendor = "Mitsubishi Electric",
|
||||
AdditionalInfo = $"Raw bytes: {resp.Payload.Length} (xem manual model để parse)"
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<SlmpResponse> SendSlmpAsync(SlmpCommand command, ReadOnlyMemory<byte> payload, CancellationToken ct = default)
|
||||
{
|
||||
if (!_connected) throw new InvalidOperationException("Chưa kết nối.");
|
||||
|
||||
// Dựng 3E frame
|
||||
var req = Build3EFrame(options, (ushort)command, subcommand: CurrentSubcommandHint, payload);
|
||||
|
||||
// Gửi/nhận với retry
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
for (int attempt = 0; attempt <= RetryCount; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rawResp = await SendAndReceiveAsync(req, ct);
|
||||
var resp = Parse3EResponse(rawResp);
|
||||
|
||||
sw.Stop();
|
||||
TrackRtt(sw.Elapsed);
|
||||
|
||||
_status = _status with
|
||||
{
|
||||
LinkUp = true,
|
||||
ConsecutiveErrors = 0,
|
||||
LastEndCode = resp.EndCode,
|
||||
LastErrorText = null,
|
||||
AvgRtt = TimeSpan.FromTicks((_rttCount > 0) ? _rttSumTicks / _rttCount : 0),
|
||||
MaxRtt = (_status.MaxRtt == null || sw.Elapsed > _status.MaxRtt) ? sw.Elapsed : _status.MaxRtt
|
||||
};
|
||||
return resp;
|
||||
}
|
||||
catch (Exception ex) when (attempt < RetryCount)
|
||||
{
|
||||
_status = _status with
|
||||
{
|
||||
LinkUp = false,
|
||||
ConsecutiveErrors = _status.ConsecutiveErrors + 1,
|
||||
LastErrorText = ex.Message,
|
||||
LastEndCode = SlmpEndCode.TransportError
|
||||
};
|
||||
// thử lại
|
||||
await Task.Delay(10, ct);
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
throw new TimeoutException("SLMP: Hết retry mà vẫn lỗi.");
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisconnectAsync();
|
||||
_sendLock.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void StartPolling(PollOptions options, CancellationToken ct = default)
|
||||
{
|
||||
StopPollingInternal();
|
||||
_pollCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
var token = _pollCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Strategy: gom vùng bit và word/dword riêng để tối ưu
|
||||
while (!token.IsCancellationRequested && _connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Đơn giản: chỉ đọc word; nếu có bit thì đọc riêng
|
||||
var wordsAreas = options.Areas;
|
||||
ushort[]? words = null;
|
||||
bool[]? bits = null;
|
||||
|
||||
// (Minh hoạ) Đọc tất cả vùng word liên tiếp bằng cách gộp, bạn có thể tối ưu thêm theo thiết bị.
|
||||
if (wordsAreas.Length > 0)
|
||||
{
|
||||
// Đây đọc lần lượt từng vùng cho đơn giản (dễ hiểu; bạn có thể hợp nhất để giảm gói)
|
||||
var aggWords = new System.Collections.Generic.List<ushort>();
|
||||
foreach (var (Device, Start, Count) in wordsAreas)
|
||||
{
|
||||
if (Device == DeviceCode.X || Device == DeviceCode.Y || Device == DeviceCode.M ||
|
||||
Device == DeviceCode.L || Device == DeviceCode.S || Device == DeviceCode.B)
|
||||
{
|
||||
// Bit area
|
||||
var b = await ReadBitsAsync(Device, Start, Count, token);
|
||||
// Nối bit (minh hoạ): không ghép; đẩy riêng
|
||||
bits = b;
|
||||
}
|
||||
else
|
||||
{
|
||||
var w = await ReadWordsAsync(Device, Start, Count, token);
|
||||
aggWords.AddRange(w);
|
||||
}
|
||||
}
|
||||
if (aggWords.Count > 0) words = [.. aggWords];
|
||||
}
|
||||
|
||||
Polled?.Invoke(this, new PollUpdatedEventArgs
|
||||
{
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Bits = bits,
|
||||
Words = words,
|
||||
DWords = null
|
||||
});
|
||||
|
||||
await Task.Delay(options.Interval, token);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch
|
||||
{
|
||||
// bỏ qua vòng lỗi và tiếp tục (tuỳ nhu cầu: bạn có thể tăng backoff)
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(50), token);
|
||||
}
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
public Task StopPollingAsync(CancellationToken ct = default)
|
||||
{
|
||||
StopPollingInternal();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ====== Internal helpers ======
|
||||
|
||||
private void StopPollingInternal()
|
||||
{
|
||||
try { _pollCts?.Cancel(); } catch { }
|
||||
try { _pollCts?.Dispose(); } catch { }
|
||||
_pollCts = null;
|
||||
}
|
||||
|
||||
private static async Task<IPAddress> ResolveHostAsync(string host, CancellationToken ct)
|
||||
{
|
||||
if (IPAddress.TryParse(host, out var ip)) return ip;
|
||||
var entry = await Dns.GetHostEntryAsync(host, ct);
|
||||
foreach (var addr in entry.AddressList)
|
||||
if (addr.AddressFamily == AddressFamily.InterNetwork) return addr;
|
||||
throw new InvalidOperationException("Không tìm thấy IPv4 cho host.");
|
||||
}
|
||||
|
||||
private void TrackRtt(TimeSpan elapsed)
|
||||
{
|
||||
var ticks = elapsed.Ticks;
|
||||
System.Threading.Interlocked.Add(ref _rttSumTicks, ticks);
|
||||
System.Threading.Interlocked.Increment(ref _rttCount);
|
||||
}
|
||||
|
||||
private static void EnsureOk(SlmpResponse resp)
|
||||
{
|
||||
if (resp.EndCode != SlmpEndCode.Completed)
|
||||
throw new InvalidOperationException($"SLMP EndCode: 0x{(ushort)resp.EndCode:X4}");
|
||||
}
|
||||
|
||||
// ====== SLMP 3E Binary Build/Parse ======
|
||||
|
||||
// Gợi ý subcommand hiện hành (đặt trước khi Build3EFrame): 0x0000=word, 0x0001=bit
|
||||
private ushort CurrentSubcommandHint = 0x0000;
|
||||
|
||||
private static byte[] Build3EFrame(IeBasicClientOptions opt, ushort command, ushort subcommand, ReadOnlyMemory<byte> userData)
|
||||
{
|
||||
// Tài liệu 3E binary (TCP): Subheader 0x5000; (UDP): 0x5400
|
||||
// Header:
|
||||
// [0-1] Subheader (LE)
|
||||
// [2] Network No
|
||||
// [3] PC No (để 0x00)
|
||||
// [4-5] Module I/O No (LE)
|
||||
// [6] Multidrop No
|
||||
// [7-8] Data Length (LE) = 2(timer) + 2(cmd) + 2(subcmd) + payload.Length
|
||||
// [9-10] Timer (LE) đơn vị 250 ms hoặc 1? (tuỳ manual; thường 0x0010 ~ 4s). Ở đây lấy theo Timeout ~ ms/10.
|
||||
// [11-12] Command (LE)
|
||||
// [13-14] Subcommand (LE)
|
||||
// [15..] Payload
|
||||
|
||||
var subheader = (opt.Transport == IeBasicTransport.Udp) ? (ushort)0x5400 : (ushort)0x5000;
|
||||
var dataLen = (ushort)(2 + 2 + 2 + userData.Length); // timer + cmd + subcmd + payload
|
||||
|
||||
// Timer: convert từ Timeout ~ ms -> giá trị phù hợp. Ở đây minh hoạ: ms/10 (tuỳ manual).
|
||||
var timer = (ushort)Math.Clamp((int)(opt.Timeout.TotalMilliseconds / 10.0), 1, 0xFFFE);
|
||||
|
||||
var buf = new byte[15 + userData.Length];
|
||||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), subheader);
|
||||
buf[2] = opt.NetworkNo;
|
||||
buf[3] = 0x00; // PC No
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), opt.ModuleIoNo);
|
||||
buf[6] = opt.MultidropNo;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(7, 2), dataLen);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(9, 2), timer);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(11, 2), command);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(13, 2), subcommand);
|
||||
userData.Span.CopyTo(buf.AsSpan(15));
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
private async Task<byte[]> SendAndReceiveAsync(byte[] request, CancellationToken ct)
|
||||
{
|
||||
await _sendLock.WaitAsync(ct).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (options.Transport == IeBasicTransport.Udp)
|
||||
{
|
||||
if (_udp is null) throw new InvalidOperationException("UDP client null.");
|
||||
await _udp.SendAsync(request, request.Length);
|
||||
var recv = await _udp.ReceiveAsync(ct);
|
||||
return recv.Buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_tcpStream is null) throw new InvalidOperationException("TCP stream null.");
|
||||
|
||||
// Gửi request
|
||||
await _tcpStream.WriteAsync(request, ct);
|
||||
await _tcpStream.FlushAsync(ct);
|
||||
|
||||
// Đọc header tối thiểu 15 bytes để biết data length
|
||||
var header = new byte[15];
|
||||
await ReadExactAsync(_tcpStream, header, ct);
|
||||
|
||||
// Lấy data length
|
||||
var dataLen = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(7, 2));
|
||||
var rest = new byte[dataLen]; // bao gồm timer/cmd/subcmd/endcode/payload (ở response thay timer bằng EndCode)
|
||||
await ReadExactAsync(_tcpStream, rest, ct);
|
||||
|
||||
// Gộp lại thành frame đầy đủ cho parser
|
||||
var combined = new byte[header.Length + rest.Length];
|
||||
Buffer.BlockCopy(header, 0, combined, 0, header.Length);
|
||||
Buffer.BlockCopy(rest, 0, combined, header.Length, rest.Length);
|
||||
return combined;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken ct)
|
||||
{
|
||||
int read = 0;
|
||||
while (read < buffer.Length)
|
||||
{
|
||||
var n = await stream.ReadAsync(buffer.AsMemory(read, buffer.Length - read), ct);
|
||||
if (n == 0) throw new SocketException((int)SocketError.ConnectionReset);
|
||||
read += n;
|
||||
}
|
||||
}
|
||||
|
||||
private static SlmpResponse Parse3EResponse(byte[] frame)
|
||||
{
|
||||
// Response 3E:
|
||||
// [0-1] Subheader
|
||||
// [2] NetworkNo
|
||||
// [3] PCNo
|
||||
// [4-5] ModuleIoNo
|
||||
// [6] MultidropNo
|
||||
// [7-8] Data Length (LE)
|
||||
// [9-10] End Code (LE) <-- khác request (timer)
|
||||
// [11..] Payload
|
||||
if (frame.Length < 11)
|
||||
throw new InvalidOperationException("Frame quá ngắn.");
|
||||
|
||||
var dataLen = BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(7, 2));
|
||||
if (frame.Length < 11 + dataLen)
|
||||
throw new InvalidOperationException("Frame không đủ chiều dài.");
|
||||
|
||||
var endCode = (SlmpEndCode)BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(9, 2));
|
||||
var payload = new ReadOnlyMemory<byte>(frame, 11, dataLen - 2); // trừ endcode (2B)
|
||||
return new SlmpResponse { EndCode = endCode, Payload = payload, Timestamp = DateTimeOffset.UtcNow };
|
||||
}
|
||||
|
||||
// ====== Payload builders (3E binary) ======
|
||||
|
||||
private static byte DevCodeBinary(DeviceCode device) => device switch
|
||||
{
|
||||
// Mã phổ biến trong MC 3E binary (tham chiếu; kiểm tra manual cho chắc theo CPU):
|
||||
DeviceCode.X => 0x9C,
|
||||
DeviceCode.Y => 0x9D,
|
||||
DeviceCode.M => 0x90,
|
||||
DeviceCode.L => 0x92,
|
||||
DeviceCode.B => 0xA0,
|
||||
DeviceCode.S => 0x98,
|
||||
DeviceCode.D => 0xA8,
|
||||
DeviceCode.W => 0xB4,
|
||||
DeviceCode.R => 0xAF,
|
||||
DeviceCode.ZR => 0xB0,
|
||||
_ => 0x00
|
||||
};
|
||||
|
||||
private ReadOnlyMemory<byte> BuildBatchWriteWords(DeviceCode device, int start, ReadOnlySpan<ushort> words)
|
||||
{
|
||||
CurrentSubcommandHint = 0x0000; // word units
|
||||
|
||||
var buf = new byte[3 + 1 + 2 + words.Length * 2];
|
||||
buf[0] = (byte)(start & 0xFF);
|
||||
buf[1] = (byte)((start >> 8) & 0xFF);
|
||||
buf[2] = (byte)((start >> 16) & 0xFF);
|
||||
buf[3] = DevCodeBinary(device);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)words.Length);
|
||||
|
||||
var off = 6;
|
||||
for (int i = 0; i < words.Length; i++, off += 2)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(off, 2), words[i]);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
private ReadOnlyMemory<byte> BuildBatchWriteBits(DeviceCode device, int start, ReadOnlySpan<bool> bits)
|
||||
{
|
||||
CurrentSubcommandHint = 0x0001; // bit units
|
||||
|
||||
// Bit packing theo MC (1 bit/byte hoặc 2 bit/byte tuỳ subcmd). Ở đây dùng 1 bit/bit (1 byte/bit) để đơn giản;
|
||||
// nhiều PLC chấp nhận. Nếu cần tối ưu, gói bit theo nibble.
|
||||
var buf = new byte[3 + 1 + 2 + bits.Length];
|
||||
buf[0] = (byte)(start & 0xFF);
|
||||
buf[1] = (byte)((start >> 8) & 0xFF);
|
||||
buf[2] = (byte)((start >> 16) & 0xFF);
|
||||
buf[3] = DevCodeBinary(device);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)bits.Length);
|
||||
|
||||
for (int i = 0; i < bits.Length; i++)
|
||||
buf[6 + i] = (byte)(bits[i] ? 0x01 : 0x00);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
private static ReadOnlyMemory<byte> BuildRandomRead((DeviceCode Device, int Address)[] points)
|
||||
{
|
||||
// Format tối giản: [count(2)] + N * (addr(3) + dev(1))
|
||||
var buf = new byte[2 + points.Length * 4];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), (ushort)points.Length);
|
||||
var off = 2;
|
||||
foreach (var (Device, Address) in points)
|
||||
{
|
||||
buf[off + 0] = (byte)(Address & 0xFF);
|
||||
buf[off + 1] = (byte)((Address >> 8) & 0xFF);
|
||||
buf[off + 2] = (byte)((Address >> 16) & 0xFF);
|
||||
buf[off + 3] = DevCodeBinary(Device);
|
||||
off += 4;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private static ReadOnlyMemory<byte> BuildRandomWrite((DeviceCode Device, int Address)[] points, ReadOnlySpan<ushort> values)
|
||||
{
|
||||
// [count(2)] + N*(addr(3)+dev(1)+value(2))
|
||||
var buf = new byte[2 + points.Length * (4 + 2)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), (ushort)points.Length);
|
||||
var off = 2;
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
{
|
||||
var (Device, Address) = points[i];
|
||||
buf[off + 0] = (byte)(Address & 0xFF);
|
||||
buf[off + 1] = (byte)((Address >> 8) & 0xFF);
|
||||
buf[off + 2] = (byte)((Address >> 16) & 0xFF);
|
||||
buf[off + 3] = DevCodeBinary(Device);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(off + 4, 2), values[i]);
|
||||
off += 6;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private static bool[] UnpackBits(ReadOnlySpan<byte> data, int expectedCount)
|
||||
{
|
||||
// Đơn giản hoá: nếu payload trả về mỗi bit = 1 byte (0x00/0x01)
|
||||
// Nếu PLC trả về packed bits, bạn cần giải theo bit-level. Ở đây xử lý cả hai:
|
||||
if (data.Length == expectedCount)
|
||||
{
|
||||
var res = new bool[expectedCount];
|
||||
for (int i = 0; i < expectedCount; i++) res[i] = data[i] != 0;
|
||||
return res;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Giải packed: LSB-first trong từng byte
|
||||
var res = new bool[expectedCount];
|
||||
int idx = 0;
|
||||
for (int b = 0; b < data.Length && idx < expectedCount; b++)
|
||||
{
|
||||
byte val = data[b];
|
||||
for (int bit = 0; bit < 8 && idx < expectedCount; bit++, idx++)
|
||||
res[idx] = ((val >> bit) & 0x01) != 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
private ReadOnlyMemory<byte> BuildBatchRead(DeviceCode device, int start, int count, bool isBit, bool isDWord = false)
|
||||
{
|
||||
// Request data:
|
||||
// [0-2] Address (3 bytes, little-endian 24-bit)
|
||||
// [3] Device Code (1 byte)
|
||||
// [4-5] Points/Count (LE, word/bit/dword tuỳ subcmd)
|
||||
// [6-7] Reserved? (tuỳ lệnh) - KHÔNG cần cho 0x0401
|
||||
// Ở đây dùng định dạng tối giản: addr(3) + dev(1) + count(2) + subcmd đã set ở header
|
||||
|
||||
if (isDWord)
|
||||
CurrentSubcommandHint = 0x0002; // dword units
|
||||
else
|
||||
CurrentSubcommandHint = isBit ? (ushort)0x0001 : (ushort)0x0000;
|
||||
|
||||
var buf = new byte[3 + 1 + 2];
|
||||
// 24-bit little-endian
|
||||
buf[0] = (byte)(start & 0xFF);
|
||||
buf[1] = (byte)((start >> 8) & 0xFF);
|
||||
buf[2] = (byte)((start >> 16) & 0xFF);
|
||||
buf[3] = DevCodeBinary(device);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)count);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// 5. Hàm kiểm tra device có hỗ trợ đọc dword trực tiếp không (tuỳ CPU, ví dụ D, W, R, ZR thường hỗ trợ)
|
||||
private static bool SupportsDirectDWordRead(DeviceCode device)
|
||||
{
|
||||
return device == DeviceCode.D || device == DeviceCode.W || device == DeviceCode.R || device == DeviceCode.ZR;
|
||||
}
|
||||
}
|
||||
488
RobotNet.ScriptManager/Connections/ModbusTcpClient.cs
Normal file
488
RobotNet.ScriptManager/Connections/ModbusTcpClient.cs
Normal file
@@ -0,0 +1,488 @@
|
||||
using RobotNet.Script;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace RobotNet.ScriptManager.Connections;
|
||||
|
||||
public sealed class ModbusTcpClient(string host, int port = 502, byte unitId = 1) : IModbusTcpClient
|
||||
{
|
||||
public string Host { get; } = host;
|
||||
public int Port { get; } = port;
|
||||
public byte UnitId { get; } = unitId;
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(2);
|
||||
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(3);
|
||||
public int MaxRetries { get; set; } = 2; // per request
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
public bool KeepAlive { get; set; } = true; // enable TCP keepalive
|
||||
public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||
public ushort HeartbeatRegisterAddress { get; set; } = 0; // dummy read address for heartbeat
|
||||
|
||||
private TcpClient? _client;
|
||||
private NetworkStream? _stream;
|
||||
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
||||
private int _transactionId = 0;
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _heartbeatTask;
|
||||
|
||||
#region Connect/Dispose
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
var cts = _cts;
|
||||
|
||||
_client = new TcpClient
|
||||
{
|
||||
NoDelay = true,
|
||||
LingerState = new LingerOption(true, 0)
|
||||
};
|
||||
|
||||
using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
connectCts.CancelAfter(ConnectTimeout);
|
||||
var connectTask = _client.ConnectAsync(Host, Port);
|
||||
using (connectCts.Token.Register(() => SafeCancel(connectTask)))
|
||||
{
|
||||
await connectTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (KeepAlive)
|
||||
{
|
||||
try
|
||||
{
|
||||
SetKeepAlive(_client.Client, true, keepAliveTimeMs: 15_000, keepAliveIntervalMs: 5_000);
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
_stream = _client.GetStream();
|
||||
_stream.ReadTimeout = (int)RequestTimeout.TotalMilliseconds;
|
||||
_stream.WriteTimeout = (int)RequestTimeout.TotalMilliseconds;
|
||||
|
||||
if (HeartbeatInterval > TimeSpan.Zero)
|
||||
{
|
||||
_heartbeatTask = Task.Run(() => HeartbeatLoopAsync(cts!.Token), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsConnected => _client?.Connected == true && _stream != null;
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
_sendLock.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
try { _cts?.Cancel(); } catch { }
|
||||
try { if (_heartbeatTask != null) await Task.WhenAny(_heartbeatTask, Task.Delay(50)); } catch { }
|
||||
try { _stream?.Dispose(); } catch { }
|
||||
try { _client?.Close(); } catch { }
|
||||
_stream = null;
|
||||
_client = null;
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_heartbeatTask = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Modbus API
|
||||
// Coils & Discretes
|
||||
public Task<bool[]> ReadCoilsAsync(ushort startAddress, ushort count, CancellationToken ct = default)
|
||||
=> ReadBitsAsync(0x01, startAddress, count, ct);
|
||||
|
||||
public Task<bool[]> ReadDiscreteInputsAsync(ushort startAddress, ushort count, CancellationToken ct = default)
|
||||
=> ReadBitsAsync(0x02, startAddress, count, ct);
|
||||
|
||||
// Registers
|
||||
public Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort count, CancellationToken ct = default)
|
||||
=> ReadRegistersAsync(0x03, startAddress, count, ct);
|
||||
|
||||
public Task<ushort[]> ReadInputRegistersAsync(ushort startAddress, ushort count, CancellationToken ct = default)
|
||||
=> ReadRegistersAsync(0x04, startAddress, count, ct);
|
||||
|
||||
public Task WriteSingleCoilAsync(ushort address, bool value, CancellationToken ct = default)
|
||||
=> WriteSingleAsync(0x05, address, value ? (ushort)0xFF00 : (ushort)0x0000, ct);
|
||||
|
||||
public Task WriteSingleRegisterAsync(ushort address, ushort value, CancellationToken ct = default)
|
||||
=> WriteSingleAsync(0x06, address, value, ct);
|
||||
|
||||
public Task WriteMultipleCoilsAsync(ushort startAddress, bool[] values, CancellationToken ct = default)
|
||||
=> WriteMultipleCoilsCoreAsync(startAddress, values, ct);
|
||||
|
||||
public Task WriteMultipleRegistersAsync(ushort startAddress, ushort[] values, CancellationToken ct = default)
|
||||
=> WriteMultipleRegistersCoreAsync(startAddress, values, ct);
|
||||
|
||||
// FC 22: Mask Write Register
|
||||
public Task MaskWriteRegisterAsync(ushort address, ushort andMask, ushort orMask, CancellationToken ct = default)
|
||||
=> SendExpectEchoAsync(0x16, writer: span =>
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span[..2], address);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), andMask);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(4, 2), orMask);
|
||||
return 6;
|
||||
}, expectedEchoLength: 6, ct);
|
||||
|
||||
// FC 23: Read/Write Multiple Registers
|
||||
public async Task<ushort[]> ReadWriteMultipleRegistersAsync(
|
||||
ushort readStart, ushort readCount,
|
||||
ushort writeStart, IReadOnlyList<ushort> writeValues,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return await SendAsync(0x17, readCount * 2, span =>
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span[..2], readStart);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), readCount);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(4, 2), writeStart);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(6, 2), (ushort)writeValues.Count);
|
||||
span[8] = (byte)(writeValues.Count * 2);
|
||||
int pos = 9;
|
||||
for (int i = 0; i < writeValues.Count; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(pos, 2), writeValues[i]);
|
||||
pos += 2;
|
||||
}
|
||||
return pos; // payload length
|
||||
}, parse: resp =>
|
||||
{
|
||||
int byteCount = resp[0];
|
||||
if (resp.Length != byteCount + 1) throw new ModbusException("Invalid byte count in response");
|
||||
var result = new ushort[byteCount / 2];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
result[i] = BinaryPrimitives.ReadUInt16BigEndian(resp.Slice(1 + i * 2, 2));
|
||||
return result;
|
||||
}, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// FC 43/14: Read Device Identification (basic)
|
||||
public async Task<Dictionary<byte, string>> ReadDeviceIdentificationAsync(byte category = 0x01 /* Basic */ , CancellationToken ct = default)
|
||||
{
|
||||
return await SendAsync(0x2B, 0, span =>
|
||||
{
|
||||
span[0] = 0x0E; // MEI type
|
||||
span[1] = 0x01; // Read Device ID
|
||||
span[2] = category; // category
|
||||
span[3] = 0x00; // object id
|
||||
return 4;
|
||||
}, parse: resp =>
|
||||
{
|
||||
// resp: [MEI, ReadDevId, conformity, moreFollows, nextObjectId, numObjects, objects...]
|
||||
if (resp.Length < 6) throw new ModbusException("Invalid Device ID response");
|
||||
int pos = 0;
|
||||
byte mei = resp[pos++];
|
||||
if (mei != 0x0E) throw new ModbusException("Invalid MEI type");
|
||||
pos++; // ReadDevId
|
||||
pos++; // conformity
|
||||
pos++; // moreFollows
|
||||
pos++; // nextObjectId
|
||||
byte num = resp[pos++];
|
||||
var dict = new Dictionary<byte, string>();
|
||||
for (int i = 0; i < num; i++)
|
||||
{
|
||||
byte id = resp[pos++];
|
||||
byte len = resp[pos++];
|
||||
if (pos + len > resp.Length) throw new ModbusException("Invalid object length");
|
||||
string val = System.Text.Encoding.ASCII.GetString(resp.ToArray(), pos, len);
|
||||
pos += len;
|
||||
dict[id] = val;
|
||||
}
|
||||
return dict;
|
||||
}, ct).ConfigureAwait(false);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Core Send Helpers
|
||||
private async Task<bool[]> ReadBitsAsync(byte function, ushort startAddress, ushort count, CancellationToken ct)
|
||||
{
|
||||
if (count is 0 or > 2000) throw new ArgumentOutOfRangeException(nameof(count));
|
||||
var data = await SendAsync(function, expectedLength: (count + 7) / 8 + 1, writer: span =>
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span[..2], startAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), count);
|
||||
return 4;
|
||||
}, parse: resp => resp.ToArray(), ct).ConfigureAwait(false);
|
||||
|
||||
int byteCount = data[0];
|
||||
if (byteCount != data.Length - 1) throw new ModbusException("Unexpected byte count");
|
||||
var result = new bool[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int b = data[1 + (i / 8)];
|
||||
result[i] = ((b >> (i % 8)) & 0x01) == 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<ushort[]> ReadRegistersAsync(byte function, ushort startAddress, ushort count, CancellationToken ct)
|
||||
{
|
||||
if (count is 0 or > 125) throw new ArgumentOutOfRangeException(nameof(count));
|
||||
var data = await SendAsync(function, expectedLength: count * 2 + 1, writer: span =>
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span[..2], startAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), count);
|
||||
return 4;
|
||||
}, parse: resp => resp.ToArray(), ct).ConfigureAwait(false);
|
||||
|
||||
int byteCount = data[0];
|
||||
if (byteCount != count * 2 || data.Length != byteCount + 1)
|
||||
throw new ModbusException("Unexpected byte count");
|
||||
var result = new ushort[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
result[i] = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(1 + i * 2, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
private Task WriteSingleAsync(byte function, ushort address, ushort value, CancellationToken ct)
|
||||
=> SendExpectEchoAsync(function, span =>
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span[..2], address);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), value);
|
||||
return 4;
|
||||
}, expectedEchoLength: 4, ct);
|
||||
|
||||
private Task WriteMultipleCoilsCoreAsync(ushort startAddress, bool[] values, CancellationToken ct)
|
||||
{
|
||||
if (values == null || values.Length == 0 || values.Length > 1968)
|
||||
throw new ArgumentOutOfRangeException(nameof(values));
|
||||
|
||||
int byteCount = (values.Length + 7) / 8;
|
||||
return SendExpectEchoAsync(0x0F, span =>
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span[..2], startAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), (ushort)values.Length);
|
||||
span[4] = (byte)byteCount;
|
||||
int pos = 5;
|
||||
int bit = 0;
|
||||
for (int i = 0; i < byteCount; i++)
|
||||
{
|
||||
byte b = 0;
|
||||
for (int j = 0; j < 8 && bit < values.Length; j++, bit++)
|
||||
if (values[bit]) b |= (byte)(1 << j);
|
||||
span[pos++] = b;
|
||||
}
|
||||
return pos;
|
||||
}, expectedEchoLength: 4, ct);
|
||||
}
|
||||
|
||||
private Task WriteMultipleRegistersCoreAsync(ushort startAddress, ushort[] values, CancellationToken ct)
|
||||
{
|
||||
if (values == null || values.Length == 0 || values.Length > 123)
|
||||
throw new ArgumentOutOfRangeException(nameof(values));
|
||||
|
||||
return SendExpectEchoAsync(0x10, span =>
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span[..2], startAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), (ushort)values.Length);
|
||||
span[4] = (byte)(values.Length * 2);
|
||||
int pos = 5;
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16BigEndian(span.Slice(pos, 2), values[i]);
|
||||
pos += 2;
|
||||
}
|
||||
return pos;
|
||||
}, expectedEchoLength: 4, ct);
|
||||
}
|
||||
|
||||
private async Task SendExpectEchoAsync(byte function, Func<Span<byte>, int> writer, int expectedEchoLength, CancellationToken ct)
|
||||
{
|
||||
_ = await SendAsync(function, expectedEchoLength + 0, writer: writer, parse: resp =>
|
||||
{
|
||||
if (resp.Length != expectedEchoLength) throw new ModbusException("Unexpected echo length");
|
||||
return 0;
|
||||
}, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<T> SendAsync<T>(byte function, int expectedLength, Func<Span<byte>, int> writer, Func<ReadOnlySpan<byte>, T> parse, CancellationToken ct)
|
||||
{
|
||||
int attempts = 0;
|
||||
while (true)
|
||||
{
|
||||
attempts++;
|
||||
try
|
||||
{
|
||||
await EnsureConnectedAsync(ct).ConfigureAwait(false);
|
||||
await _sendLock.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var reqPayload = new byte[260]; // generous for payload
|
||||
int payloadLen = writer(reqPayload);
|
||||
var pdu = new byte[payloadLen + 1];
|
||||
pdu[0] = function;
|
||||
Buffer.BlockCopy(reqPayload, 0, pdu, 1, payloadLen);
|
||||
|
||||
var adu = BuildMbap(UnitId, pdu);
|
||||
await _stream!.WriteAsync(adu, ct).ConfigureAwait(false);
|
||||
|
||||
// Read MBAP (7 bytes), then body
|
||||
byte[] mbap = await ReadExactAsync(7, ct).ConfigureAwait(false);
|
||||
ushort transId = BinaryPrimitives.ReadUInt16BigEndian(mbap.AsSpan(0, 2));
|
||||
ushort proto = BinaryPrimitives.ReadUInt16BigEndian(mbap.AsSpan(2, 2));
|
||||
ushort len = BinaryPrimitives.ReadUInt16BigEndian(mbap.AsSpan(4, 2));
|
||||
byte unit = mbap[6];
|
||||
if (proto != 0 || unit != UnitId) throw new ModbusException("Invalid MBAP header");
|
||||
if (len < 2) throw new ModbusException("Invalid length");
|
||||
|
||||
byte[] body = await ReadExactAsync(len - 1, ct).ConfigureAwait(false); // len includes unitId
|
||||
byte fc = body[0];
|
||||
if ((fc & 0x80) != 0)
|
||||
{
|
||||
byte ex = body[1];
|
||||
throw new ModbusException($"Exception (FC={(function):X2}): {ex}", (ModbusExceptionCode)ex);
|
||||
}
|
||||
if (fc != function) throw new ModbusException("Mismatched function in response");
|
||||
|
||||
var pduData = body.AsSpan(1);
|
||||
// If caller supplied an expectedLength, do a soft sanity check when applicable
|
||||
if (expectedLength > 0 && pduData.Length < expectedLength)
|
||||
{
|
||||
// some functions have variable lengths; we avoid hard-failing here
|
||||
}
|
||||
return parse(pduData);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (attempts <= MaxRetries && IsTransient(ex))
|
||||
{
|
||||
if (AutoReconnect)
|
||||
{
|
||||
await ReconnectAsync(ct).ConfigureAwait(false);
|
||||
continue; // retry
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureConnectedAsync(CancellationToken ct)
|
||||
{
|
||||
if (IsConnected) return;
|
||||
await ConnectAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ReconnectAsync(CancellationToken ct)
|
||||
{
|
||||
try { await CloseAsync(); } catch { }
|
||||
await Task.Delay(100, ct).ConfigureAwait(false);
|
||||
await ConnectAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private byte[] BuildMbap(byte unitId, byte[] pdu)
|
||||
{
|
||||
// MBAP: Transaction(2) Protocol(2=0) Length(2) UnitId(1)
|
||||
// Length = PDU length + 1 (UnitId)
|
||||
ushort trans = unchecked((ushort)Interlocked.Increment(ref _transactionId));
|
||||
var adu = new byte[7 + pdu.Length];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(adu.AsSpan(0, 2), trans);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(adu.AsSpan(2, 2), 0);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(adu.AsSpan(4, 2), (ushort)(pdu.Length + 1));
|
||||
adu[6] = unitId;
|
||||
Buffer.BlockCopy(pdu, 0, adu, 7, pdu.Length);
|
||||
return adu;
|
||||
}
|
||||
|
||||
private async Task<byte[]> ReadExactAsync(int length, CancellationToken ct)
|
||||
{
|
||||
byte[] buf = new byte[length];
|
||||
int read = 0;
|
||||
while (read < length)
|
||||
{
|
||||
int n = await _stream!.ReadAsync(buf.AsMemory(read, length - read), ct).ConfigureAwait(false);
|
||||
if (n <= 0) throw new IOException("Remote closed the connection");
|
||||
read += n;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private static bool IsTransient(Exception ex)
|
||||
=> ex is SocketException or IOException or TimeoutException;
|
||||
|
||||
private async Task HeartbeatLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(HeartbeatInterval, ct).ConfigureAwait(false);
|
||||
if (ct.IsCancellationRequested) break;
|
||||
if (IsConnected)
|
||||
{
|
||||
// best-effort heartbeat: read one register
|
||||
using var timeoutCts = new CancellationTokenSource(RequestTimeout);
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
|
||||
await ReadHoldingRegistersAsync(HeartbeatRegisterAddress, 1, linked.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (AutoReconnect)
|
||||
{
|
||||
try { await ReconnectAsync(ct).ConfigureAwait(false); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SafeCancel(Task _)
|
||||
{
|
||||
try { } catch { }
|
||||
}
|
||||
|
||||
private static void SetKeepAlive(Socket socket, bool on, int keepAliveTimeMs, int keepAliveIntervalMs)
|
||||
{
|
||||
// Windows & Linux support via IOControl (best-effort)
|
||||
// On Linux, consider sysctl or TCP_KEEP* sockopts when available.
|
||||
if (!on) return;
|
||||
byte[] inOptionValues = new byte[12];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(inOptionValues.AsSpan(0), 1);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(inOptionValues.AsSpan(4), (uint)keepAliveTimeMs);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(inOptionValues.AsSpan(8), (uint)keepAliveIntervalMs);
|
||||
const int SIO_KEEPALIVE_VALS = -1744830460;
|
||||
try { socket.IOControl(SIO_KEEPALIVE_VALS, inOptionValues, null); } catch { }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public enum ModbusExceptionCode : byte
|
||||
{
|
||||
IllegalFunction = 0x01,
|
||||
IllegalDataAddress = 0x02,
|
||||
IllegalDataValue = 0x03,
|
||||
SlaveDeviceFailure = 0x04,
|
||||
Acknowledge = 0x05,
|
||||
SlaveDeviceBusy = 0x06,
|
||||
MemoryParityError = 0x08,
|
||||
GatewayPathUnavailable = 0x0A,
|
||||
GatewayTargetFailedToRespond = 0x0B
|
||||
}
|
||||
|
||||
public sealed class ModbusException(string message, ModbusExceptionCode? code = null) : Exception(message)
|
||||
{
|
||||
public ModbusExceptionCode? Code { get; } = code;
|
||||
|
||||
public static string DescribeException(byte code) => code switch
|
||||
{
|
||||
0x01 => "Illegal Function",
|
||||
0x02 => "Illegal Data Address",
|
||||
0x03 => "Illegal Data Value",
|
||||
0x04 => "Slave Device Failure",
|
||||
0x05 => "Acknowledge",
|
||||
0x06 => "Slave Device Busy",
|
||||
0x08 => "Memory Parity Error",
|
||||
0x0A => "Gateway Path Unavailable",
|
||||
0x0B => "Gateway Target Failed To Respond",
|
||||
_ => $"Unknown Exception 0x{code:X2}"
|
||||
};
|
||||
}
|
||||
|
||||
64
RobotNet.ScriptManager/Connections/UnixDevice.cs
Normal file
64
RobotNet.ScriptManager/Connections/UnixDevice.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using RobotNet.Script;
|
||||
|
||||
namespace RobotNet.ScriptManager.Connections;
|
||||
|
||||
public class UnixDevice : IUnixDevice
|
||||
{
|
||||
public static readonly UnixDevice Instance = new();
|
||||
|
||||
private UnixDevice() { }
|
||||
public byte[] ReadDev(string name, int length)
|
||||
{
|
||||
if (Environment.OSVersion.Platform != PlatformID.Unix)
|
||||
{
|
||||
throw new PlatformNotSupportedException("This method is only supported on linux.");
|
||||
}
|
||||
if (length <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than zero.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name) || name.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
|
||||
{
|
||||
throw new ArgumentException("Invalid device name.", nameof(name));
|
||||
}
|
||||
|
||||
var path = $"/dev/robotnet/{name}";
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException($"Device '{name}' not found.", path);
|
||||
}
|
||||
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
|
||||
var buffer = new byte[length];
|
||||
var bytesRead = fs.Read(buffer, 0, length);
|
||||
fs.Close();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void WriteDev(string name, byte[] data)
|
||||
{
|
||||
if (Environment.OSVersion.Platform != PlatformID.Unix)
|
||||
{
|
||||
throw new PlatformNotSupportedException("This method is only supported on linux.");
|
||||
}
|
||||
if (data == null || data.Length == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data), "Data cannot be null or empty.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(name) || name.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
|
||||
{
|
||||
throw new ArgumentException("Invalid device name.", nameof(name));
|
||||
}
|
||||
var path = $"/dev/robotnet/{name}";
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException($"Device '{name}' not found.", path);
|
||||
}
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.Read);
|
||||
fs.Write(data, 0, data.Length);
|
||||
fs.Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class DashboardConfigController(DashboardConfig Config, ILogger<DashboardConfigController> Logger) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public Task<MessageResult<string[]>> GetOpenACSSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Task.FromResult<MessageResult<string[]>>(new(true, "") { Data = Config.MissionNames });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Lấy cấu hình OpenACS xảy ra lỗi: {ex.Message}");
|
||||
return Task.FromResult<MessageResult<string[]>>(new(false, "Hệ thống có lỗi xảy ra"));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<MessageResult> UpdatePublishSetting([FromBody] string[] missionNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Config.UpdateMissionNames(missionNames);
|
||||
return new(true, "");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Cập nhật cấu hình publish OpenACS xảy ra lỗi: {ex.Message}");
|
||||
return new(false, "Hệ thống có lỗi xảy ra");
|
||||
}
|
||||
}
|
||||
}
|
||||
197
RobotNet.ScriptManager/Controllers/ScriptController.cs
Normal file
197
RobotNet.ScriptManager/Controllers/ScriptController.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ScriptController(ScriptStateManager scriptBuilder) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
public ScriptFolder Gets() => new("Scripts", GetScriptFolders(ScriptConfiguration.ScriptStorePath), GetScriptFiles(ScriptConfiguration.ScriptStorePath));
|
||||
|
||||
[HttpPost]
|
||||
[Route("Directory")]
|
||||
public MessageResult CreateDirectory([FromBody] CreateModel model)
|
||||
{
|
||||
if(scriptBuilder.State == ProcessorState.Building
|
||||
|| scriptBuilder.State == ProcessorState.Starting
|
||||
|| scriptBuilder.State == ProcessorState.Running
|
||||
|| scriptBuilder.State == ProcessorState.Stopping)
|
||||
{
|
||||
return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể tạo thư mục");
|
||||
}
|
||||
var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path, model.Name);
|
||||
if (Directory.Exists(fullPath)) return new(false, "Folder đã tồn tại");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(fullPath);
|
||||
return new(true, "");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new(false, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("File")]
|
||||
public MessageResult CreateFile([FromBody] CreateModel model)
|
||||
{
|
||||
if (scriptBuilder.State == ProcessorState.Building
|
||||
|| scriptBuilder.State == ProcessorState.Starting
|
||||
|| scriptBuilder.State == ProcessorState.Running
|
||||
|| scriptBuilder.State == ProcessorState.Stopping)
|
||||
{
|
||||
return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể tạo file");
|
||||
}
|
||||
var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path, model.Name);
|
||||
if (System.IO.File.Exists(fullPath)) return new(false, "File đã tồn tại");
|
||||
try
|
||||
{
|
||||
var fs = System.IO.File.Create(fullPath);
|
||||
fs.Close();
|
||||
return new(true, "");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new(false, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch]
|
||||
[Route("File")]
|
||||
public MessageResult UpdateCode([FromBody] UpdateModel model)
|
||||
{
|
||||
if (scriptBuilder.State == ProcessorState.Building
|
||||
|| scriptBuilder.State == ProcessorState.Starting
|
||||
|| scriptBuilder.State == ProcessorState.Running
|
||||
|| scriptBuilder.State == ProcessorState.Stopping)
|
||||
{
|
||||
return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể update file");
|
||||
}
|
||||
var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path);
|
||||
System.IO.File.WriteAllText(fullPath, model.Code);
|
||||
ResetScriptProcessor();
|
||||
return new(true, "");
|
||||
}
|
||||
|
||||
|
||||
[HttpPatch]
|
||||
[Route("FileName")]
|
||||
public MessageResult RenameFile([FromBody] RenameModel model)
|
||||
{
|
||||
if (scriptBuilder.State == ProcessorState.Building
|
||||
|| scriptBuilder.State == ProcessorState.Starting
|
||||
|| scriptBuilder.State == ProcessorState.Running
|
||||
|| scriptBuilder.State == ProcessorState.Stopping)
|
||||
{
|
||||
return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể thay đổi tên file");
|
||||
}
|
||||
var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path);
|
||||
if (!System.IO.File.Exists(fullPath)) return new(false, "Source code không tồn tại");
|
||||
|
||||
var folder = Path.GetDirectoryName(fullPath) ?? "";
|
||||
var fi = new FileInfo(fullPath);
|
||||
fi.MoveTo(Path.Combine(folder, model.NewName));
|
||||
return new(true, "");
|
||||
}
|
||||
|
||||
[HttpPatch]
|
||||
[Route("DirectoryName")]
|
||||
public MessageResult RenameDirectory([FromBody] RenameModel model)
|
||||
{
|
||||
if (scriptBuilder.State == ProcessorState.Building
|
||||
|| scriptBuilder.State == ProcessorState.Starting
|
||||
|| scriptBuilder.State == ProcessorState.Running
|
||||
|| scriptBuilder.State == ProcessorState.Stopping)
|
||||
{
|
||||
return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể thay đổi tên thư mục");
|
||||
}
|
||||
var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path);
|
||||
if (!Directory.Exists(fullPath)) return new(false, "Folder không tồn tại");
|
||||
|
||||
var folder = Path.GetDirectoryName(fullPath) ?? "";
|
||||
var di = new DirectoryInfo(fullPath);
|
||||
di.MoveTo(Path.Combine(folder, model.NewName));
|
||||
return new(true, "");
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("File")]
|
||||
public MessageResult DeleteFile([FromQuery] string path)
|
||||
{
|
||||
if (scriptBuilder.State == ProcessorState.Building
|
||||
|| scriptBuilder.State == ProcessorState.Starting
|
||||
|| scriptBuilder.State == ProcessorState.Running
|
||||
|| scriptBuilder.State == ProcessorState.Stopping)
|
||||
{
|
||||
return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể xóa file");
|
||||
}
|
||||
var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, path);
|
||||
if (!System.IO.File.Exists(fullPath)) return new(false, "Source code không tồn tại");
|
||||
|
||||
System.IO.File.Delete(fullPath);
|
||||
ResetScriptProcessor();
|
||||
return new(true, "");
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("Directory")]
|
||||
public MessageResult DeleteDirectory([FromQuery] string path)
|
||||
{
|
||||
if (scriptBuilder.State == ProcessorState.Building
|
||||
|| scriptBuilder.State == ProcessorState.Starting
|
||||
|| scriptBuilder.State == ProcessorState.Running
|
||||
|| scriptBuilder.State == ProcessorState.Stopping)
|
||||
{
|
||||
return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể xóa thư mục");
|
||||
}
|
||||
var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, path);
|
||||
if (!Directory.Exists(fullPath)) return new(false, "Folder không tồn tại");
|
||||
|
||||
var di = new DirectoryInfo(fullPath);
|
||||
di.Delete(true);
|
||||
ResetScriptProcessor();
|
||||
return new(true, "");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("UsingNamespaces")]
|
||||
public IEnumerable<string> GetUsingNamespaces() => ScriptConfiguration.UsingNamespaces;
|
||||
|
||||
[HttpGet]
|
||||
[Route("PreCode")]
|
||||
public string GetPreCode() => ScriptConfiguration.DeveloptGlobalsScript;
|
||||
|
||||
private void ResetScriptProcessor()
|
||||
{
|
||||
string message = string.Empty;
|
||||
scriptBuilder.Reset(ref message);
|
||||
}
|
||||
|
||||
private static IEnumerable<ScriptFile> GetScriptFiles(string parentDir)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(parentDir);
|
||||
foreach (var fileInfo in dirInfo.GetFiles())
|
||||
{
|
||||
yield return new ScriptFile(fileInfo.Name, System.IO.File.ReadAllText(fileInfo.FullName));
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ScriptFolder> GetScriptFolders(string parentDir)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(parentDir);
|
||||
foreach (var dir in dirInfo.GetDirectories())
|
||||
{
|
||||
yield return new ScriptFolder(dir.Name, GetScriptFolders(dir.FullName), GetScriptFiles(dir.FullName));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace RobotNet.ScriptManager.Controllers;
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
public class ScriptManagerLoggerController(ILogger<ScriptManagerLoggerController> Logger) : ControllerBase
|
||||
{
|
||||
private readonly string LoggerDirectory = "scriptManagerlogs";
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IEnumerable<string>> GetLogs([FromQuery(Name = "date")] DateTime date)
|
||||
{
|
||||
string temp = "";
|
||||
try
|
||||
{
|
||||
string fileName = $"{date:yyyy-MM-dd}.log";
|
||||
string path = Path.Combine(LoggerDirectory, fileName);
|
||||
if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory)))
|
||||
{
|
||||
Logger.LogWarning($"GetLogs: phát hiện đường dẫn không hợp lệ.");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!System.IO.File.Exists(path))
|
||||
{
|
||||
Logger.LogWarning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}.");
|
||||
return [];
|
||||
}
|
||||
|
||||
temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log");
|
||||
System.IO.File.Copy(path, temp);
|
||||
|
||||
return await System.IO.File.ReadAllLinesAsync(temp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
RobotNet.ScriptManager/Controllers/ScriptMissionsController.cs
Normal file
154
RobotNet.ScriptManager/Controllers/ScriptMissionsController.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace RobotNet.ScriptManager.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ScriptMissionsController(ScriptMissionManager missionManager, ScriptManagerDbContext scriptManagerDb, ScriptMissionCreator missionCreator) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
public IEnumerable<ScriptMissionDto> GetAlls() => missionManager.GetMissionDatas();
|
||||
|
||||
[HttpGet]
|
||||
[Route("Runner")]
|
||||
public async Task<SearchResult<InstanceMissionDto>> Search(
|
||||
[FromQuery(Name = "txtSearch")] string? txtSearch,
|
||||
[FromQuery(Name = "page")] int page,
|
||||
[FromQuery(Name = "size")] int size)
|
||||
{
|
||||
IOrderedQueryable<InstanceMission> query;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(txtSearch))
|
||||
{
|
||||
query = scriptManagerDb.InstanceMissions.AsQueryable().OrderByDescending(x => x.CreatedAt);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = scriptManagerDb.InstanceMissions.AsQueryable().Where(x => x.MissionName.Contains(txtSearch)).OrderByDescending(x => x.CreatedAt);
|
||||
}
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
// Đảm bảo size hợp lệ
|
||||
if (size <= 0) size = 10;
|
||||
|
||||
// Tính tổng số trang
|
||||
var totalPages = (int)Math.Ceiling(total / (double)size);
|
||||
|
||||
// Đảm bảo page hợp lệ
|
||||
if (page <= 0) page = 1;
|
||||
if (page > totalPages && totalPages > 0) page = totalPages;
|
||||
|
||||
// Nếu không có dữ liệu, trả về rỗng
|
||||
var items = Enumerable.Empty<InstanceMissionDto>();
|
||||
if (total > 0)
|
||||
{
|
||||
items = [.. await query
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Skip((page - 1) * size)
|
||||
.Take(size)
|
||||
.Select(x => new InstanceMissionDto
|
||||
{
|
||||
Id = x.Id,
|
||||
MissionName = x.MissionName,
|
||||
Parameters = x.Parameters,
|
||||
CreatedAt = x.CreatedAt,
|
||||
Status = x.Status,
|
||||
Log = x.Log
|
||||
})
|
||||
.ToListAsync()];
|
||||
}
|
||||
|
||||
return new SearchResult<InstanceMissionDto>
|
||||
{
|
||||
Total = total,
|
||||
Page = page,
|
||||
Size = size,
|
||||
Items = items
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("Runner")]
|
||||
public async Task<MessageResult<Guid>> Create(InstanceMissionCreateModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = await missionCreator.CreateMissionAsync(model.Name, model.Parameters);
|
||||
return new(true, "Tạo nhiệm vụ thành công") { Data = id };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new MessageResult<Guid>(false, $"Lỗi khi tạo nhiệm vụ: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("Runner/{id:guid}")]
|
||||
public async Task<MessageResult> Cancel(Guid id, [FromQuery] string? reason)
|
||||
{
|
||||
if (missionManager.Cancel(id, $"Cancel by user '{HttpContext.User.FindFirst(ClaimTypes.Name)?.Value??HttpContext.User.Identity?.Name}' with reason '{reason}'"))
|
||||
{
|
||||
var mission = await scriptManagerDb.InstanceMissions.FindAsync(id);
|
||||
if (mission != null)
|
||||
{
|
||||
mission.Status = MissionStatus.Canceling;
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
}
|
||||
return new MessageResult(true, "Gửi yêu cầu hủy nhiệm vụ thành công");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new MessageResult(false, "Không thể hủy nhiệm vụ này hoặc nhiệm vụ không tồn tại");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Route("Runner/{id:guid}/pause")]
|
||||
public async Task<MessageResult> Pause(Guid id)
|
||||
{
|
||||
if (missionManager.Pause(id))
|
||||
{
|
||||
var mission = await scriptManagerDb.InstanceMissions.FindAsync(id);
|
||||
if (mission != null)
|
||||
{
|
||||
mission.Status = MissionStatus.Pausing;
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
}
|
||||
return new MessageResult(true, "Gửi yêu cầu tạm dừng nhiệm vụ thành công");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new MessageResult(false, "Không thể tạm dừng nhiệm vụ này hoặc nhiệm vụ không tồn tại");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Route("Runner/{id:guid}/resume")]
|
||||
public async Task<MessageResult> Resume(Guid id)
|
||||
{
|
||||
if (missionManager.Resume(id))
|
||||
{
|
||||
var mission = await scriptManagerDb.InstanceMissions.FindAsync(id);
|
||||
if (mission != null)
|
||||
{
|
||||
mission.Status = MissionStatus.Resuming;
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
}
|
||||
return new MessageResult(true, "Gửi yêu cầu tiếp tục nhiệm vụ thành công");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new MessageResult(false, "Không thể tiếp tục nhiệm vụ này hoặc nhiệm vụ không tồn tại");
|
||||
}
|
||||
}
|
||||
}
|
||||
18
RobotNet.ScriptManager/Controllers/ScriptTasksController.cs
Normal file
18
RobotNet.ScriptManager/Controllers/ScriptTasksController.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ScriptTasksController(ScriptTaskManager taskManager) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
public IEnumerable<ScriptTaskDto> GetAlls() => taskManager.GetTaskDatas();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ScriptVariablesController(ScriptGlobalsManager globalManager) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
public IEnumerable<ScriptVariableDto> GetAlls() => globalManager.GetVariablesData();
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{name}")]
|
||||
public MessageResult<string> GetVariableValue(string name)
|
||||
{
|
||||
if (globalManager.Globals.TryGetValue(name, out var value))
|
||||
{
|
||||
return new (true, "") { Data = value?.ToString() ?? "null" };
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(false, $"Variable \"{name}\" not found.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Route("{name}")]
|
||||
public MessageResult UpdateVariableValue(string name, [FromBody] UpdateVariableModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(name != model.Name)
|
||||
{
|
||||
return new MessageResult(false, "Variable name in the URL does not match the name in the body.");
|
||||
}
|
||||
|
||||
globalManager.SetValue(model.Name, model.Value);
|
||||
return new MessageResult(true, $"Variable \"{model.Name}\" updated {model.Value} successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new MessageResult(false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Route("{name}/Reset")]
|
||||
public MessageResult ResetVariableValue(string name, [FromBody] UpdateVariableModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (name != model.Name)
|
||||
{
|
||||
return new MessageResult(false, "Variable name in the URL does not match the name in the body.");
|
||||
}
|
||||
|
||||
globalManager.ResetValue(model.Name);
|
||||
return new MessageResult(true, $"Variable \"{model.Name}\" reset successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new MessageResult(false, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
RobotNet.ScriptManager/Data/InstanceMission.cs
Normal file
39
RobotNet.ScriptManager/Data/InstanceMission.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using RobotNet.Script.Shares;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace RobotNet.ScriptManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
[Table("InstanceMissions")]
|
||||
public class InstanceMission
|
||||
{
|
||||
[Column("Id", TypeName = "uniqueidentifier")]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
[Key]
|
||||
[Required]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Column("MissionName", TypeName = "varchar(126)")]
|
||||
[Required]
|
||||
public string MissionName { get; set; }
|
||||
|
||||
[Column("CreatedAt", TypeName = "datetime2")]
|
||||
[Required]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[Column("Parameters", TypeName = "nvarchar(max)")]
|
||||
public string Parameters { get; set; }
|
||||
|
||||
[Column("Status", TypeName = "int")]
|
||||
[Required]
|
||||
public MissionStatus Status { get; set; }
|
||||
|
||||
[Column("StopedAt", TypeName = "datetime2")]
|
||||
[Required]
|
||||
public DateTime StopedAt { get; set; }
|
||||
|
||||
[Column("Log", TypeName = "nvarchar(max)")]
|
||||
public string Log { get; set; }
|
||||
}
|
||||
61
RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.Designer.cs
generated
Normal file
61
RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.Designer.cs
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.ScriptManager.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ScriptManagerDbContext))]
|
||||
[Migration("20250630100458_InitScriptManagerDbContext")]
|
||||
partial class InitScriptManagerDbContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RobotNet.ScriptManager.Data.InstanceMission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("Id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreatedAt");
|
||||
|
||||
b.Property<string>("Log")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Log");
|
||||
|
||||
b.Property<string>("MissionName")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(126)")
|
||||
.HasColumnName("MissionName");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("Status");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("InstanceMissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.ScriptManager.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitScriptManagerDbContext : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InstanceMissions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
MissionName = table.Column<string>(type: "varchar(126)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
Log = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_InstanceMissions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InstanceMissions_CreatedAt",
|
||||
table: "InstanceMissions",
|
||||
column: "CreatedAt");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "InstanceMissions");
|
||||
}
|
||||
}
|
||||
}
|
||||
65
RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.Designer.cs
generated
Normal file
65
RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.Designer.cs
generated
Normal file
@@ -0,0 +1,65 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.ScriptManager.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ScriptManagerDbContext))]
|
||||
[Migration("20250701130724_AddParametersToInstanceMission")]
|
||||
partial class AddParametersToInstanceMission
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RobotNet.ScriptManager.Data.InstanceMission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("Id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreatedAt");
|
||||
|
||||
b.Property<string>("Log")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Log");
|
||||
|
||||
b.Property<string>("MissionName")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(126)")
|
||||
.HasColumnName("MissionName");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Parameters");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("Status");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("InstanceMissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.ScriptManager.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddParametersToInstanceMission : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Parameters",
|
||||
table: "InstanceMissions",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Parameters",
|
||||
table: "InstanceMissions");
|
||||
}
|
||||
}
|
||||
}
|
||||
69
RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.Designer.cs
generated
Normal file
69
RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.Designer.cs
generated
Normal file
@@ -0,0 +1,69 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.ScriptManager.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ScriptManagerDbContext))]
|
||||
[Migration("20250814072909_AddStopedAtToInstanceMission")]
|
||||
partial class AddStopedAtToInstanceMission
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RobotNet.ScriptManager.Data.InstanceMission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("Id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreatedAt");
|
||||
|
||||
b.Property<string>("Log")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Log");
|
||||
|
||||
b.Property<string>("MissionName")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(126)")
|
||||
.HasColumnName("MissionName");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Parameters");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("Status");
|
||||
|
||||
b.Property<DateTime>("StopedAt")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("StopedAt");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("InstanceMissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.ScriptManager.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStopedAtToInstanceMission : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "StopedAt",
|
||||
table: "InstanceMissions",
|
||||
type: "datetime2",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StopedAt",
|
||||
table: "InstanceMissions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet.ScriptManager.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ScriptManagerDbContext))]
|
||||
partial class ScriptManagerDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RobotNet.ScriptManager.Data.InstanceMission", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("Id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreatedAt");
|
||||
|
||||
b.Property<string>("Log")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Log");
|
||||
|
||||
b.Property<string>("MissionName")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(126)")
|
||||
.HasColumnName("MissionName");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Parameters");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("Status");
|
||||
|
||||
b.Property<DateTime>("StopedAt")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("StopedAt");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("InstanceMissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
16
RobotNet.ScriptManager/Data/ScriptManagerDbContext.cs
Normal file
16
RobotNet.ScriptManager/Data/ScriptManagerDbContext.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace RobotNet.ScriptManager.Data;
|
||||
|
||||
public class ScriptManagerDbContext(DbContextOptions<ScriptManagerDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<InstanceMission> InstanceMissions { get; private set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<InstanceMission>()
|
||||
.HasIndex(im => im.CreatedAt);
|
||||
}
|
||||
}
|
||||
17
RobotNet.ScriptManager/Data/ScriptManagerDbExtensions.cs
Normal file
17
RobotNet.ScriptManager/Data/ScriptManagerDbExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace RobotNet.ScriptManager.Data;
|
||||
|
||||
public static class ScriptManagerDbExtensions
|
||||
{
|
||||
public static async Task SeedScriptManagerDbAsync(this IServiceProvider serviceProvider)
|
||||
{
|
||||
using var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||
|
||||
using var appDb = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();
|
||||
|
||||
await appDb.Database.MigrateAsync();
|
||||
//await appDb.Database.EnsureCreatedAsync();
|
||||
await appDb.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
105
RobotNet.ScriptManager/Dockerfile
Normal file
105
RobotNet.ScriptManager/Dockerfile
Normal file
@@ -0,0 +1,105 @@
|
||||
FROM alpine:3.22 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY ["RobotNet.ScriptManager/RobotNet.ScriptManager.csproj", "RobotNet.ScriptManager/"]
|
||||
COPY ["RobotNet.Script.Shares/RobotNet.Script.Shares.csproj", "RobotNet.Script.Shares/"]
|
||||
COPY ["RobotNet.RobotShares/RobotNet.RobotShares.csproj", "RobotNet.RobotShares/"]
|
||||
COPY ["RobotNet.MapShares/RobotNet.MapShares.csproj", "RobotNet.MapShares/"]
|
||||
COPY ["RobotNet.Script/RobotNet.Script.csproj", "RobotNet.Script/"]
|
||||
COPY ["RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj", "RobotNet.Script.Expressions/"]
|
||||
COPY ["RobotNet.Shares/RobotNet.Shares.csproj", "RobotNet.Shares/"]
|
||||
COPY ["RobotNet.OpenIddictClient/RobotNet.OpenIddictClient.csproj", "RobotNet.OpenIddictClient/"]
|
||||
COPY ["RobotNet.Clients/RobotNet.Clients.csproj", "RobotNet.Clients/"]
|
||||
|
||||
# RUN dotnet package remove "Microsoft.EntityFrameworkCore.Tools" --project "RobotNet.ScriptManager/RobotNet.ScriptManager.csproj"
|
||||
RUN dotnet restore "RobotNet.ScriptManager/RobotNet.ScriptManager.csproj"
|
||||
|
||||
COPY RobotNet.Script/ RobotNet.Script/
|
||||
COPY RobotNet.Script.Expressions/ RobotNet.Script.Expressions/
|
||||
RUN rm -rf ./RobotNet.Script/bin
|
||||
RUN rm -rf ./RobotNet.Script/obj
|
||||
RUN rm -rf ./RobotNet.Script.Expressions/bin
|
||||
RUN rm -rf ./RobotNet.Script.Expressions/obj
|
||||
|
||||
WORKDIR "/src/RobotNet.Script"
|
||||
RUN dotnet build "RobotNet.Script.csproj" -c Release -o /app/script/
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY RobotNet.ScriptManager/ RobotNet.ScriptManager/
|
||||
COPY RobotNet.Script.Shares/ RobotNet.Script.Shares/
|
||||
COPY RobotNet.RobotShares/ RobotNet.RobotShares/
|
||||
COPY RobotNet.MapShares/ RobotNet.MapShares/
|
||||
COPY RobotNet.Script/ RobotNet.Script/
|
||||
COPY RobotNet.Shares/ RobotNet.Shares/
|
||||
COPY RobotNet.OpenIddictClient/ RobotNet.OpenIddictClient/
|
||||
COPY RobotNet.Clients/ RobotNet.Clients/
|
||||
|
||||
RUN rm -rf ./RobotNet.ScriptManager/bin
|
||||
RUN rm -rf ./RobotNet.ScriptManager/obj
|
||||
RUN rm -rf ./RobotNet.Script.Shares/bin
|
||||
RUN rm -rf ./RobotNet.Script.Shares/obj
|
||||
RUN rm -rf ./RobotNet.RobotShares/bin
|
||||
RUN rm -rf ./RobotNet.RobotShares/obj
|
||||
RUN rm -rf ./RobotNet.MapShares/bin
|
||||
RUN rm -rf ./RobotNet.MapShares/obj
|
||||
RUN rm -rf ./RobotNet.Script/bin
|
||||
RUN rm -rf ./RobotNet.Script/obj
|
||||
RUN rm -rf ./RobotNet.Shares/bin
|
||||
RUN rm -rf ./RobotNet.Shares/obj
|
||||
RUN rm -rf ./RobotNet.OpenIddictClient/bin
|
||||
RUN rm -rf ./RobotNet.OpenIddictClient/obj
|
||||
RUN rm -rf ./RobotNet.Clients/bin
|
||||
RUN rm -rf ./RobotNet.Clients/obj
|
||||
|
||||
RUN rm /src/RobotNet.ScriptManager/wwwroot/dlls/*
|
||||
RUN cp /app/script/RobotNet.Script.dll /src/RobotNet.ScriptManager/wwwroot/dlls/
|
||||
RUN cp /app/script/RobotNet.Script.xml /src/RobotNet.ScriptManager/wwwroot/dlls/
|
||||
RUN cp /app/script/RobotNet.Script.Expressions.dll /src/RobotNet.ScriptManager/wwwroot/dlls/
|
||||
RUN cp /app/script/RobotNet.Script.Expressions.xml /src/RobotNet.ScriptManager/wwwroot/dlls/
|
||||
RUN cp /usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/$DOTNET_VERSION/ref/net9.0/System.Collections.dll /src/RobotNet.ScriptManager/wwwroot/dlls/
|
||||
RUN cp /usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/$DOTNET_VERSION/ref/net9.0/System.Linq.Expressions.dll /src/RobotNet.ScriptManager/wwwroot/dlls/
|
||||
RUN cp /usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/$DOTNET_VERSION/ref/net9.0/System.Runtime.dll /src/RobotNet.ScriptManager/wwwroot/dlls/
|
||||
|
||||
WORKDIR "/src/RobotNet.ScriptManager"
|
||||
RUN dotnet build "RobotNet.ScriptManager.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
WORKDIR /src/RobotNet.ScriptManager
|
||||
RUN dotnet publish "RobotNet.ScriptManager.csproj" \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
--runtime linux-musl-x64 \
|
||||
--self-contained true \
|
||||
/p:PublishTrimmed=false \
|
||||
/p:PublishReadyToRun=true
|
||||
|
||||
WORKDIR /app/publish
|
||||
RUN mkdir -p /app/publish/scripts
|
||||
RUN mkdir -p /app/publish/dlls
|
||||
RUN cp /app/build/RobotNet.Script.dll /app/publish/dlls/RobotNet.Script.dll
|
||||
RUN cp /app/build/RobotNet.Script.Expressions.dll /app/publish/dlls/RobotNet.Script.Expressions.dll
|
||||
RUN cp /usr/share/dotnet/shared/Microsoft.NETCore.App/$DOTNET_VERSION/System.Linq.Expressions.dll /app/publish/dlls/
|
||||
RUN cp /usr/share/dotnet/shared/Microsoft.NETCore.App/$DOTNET_VERSION/System.Private.CoreLib.dll /app/publish/dlls/
|
||||
RUN cp /usr/share/dotnet/shared/Microsoft.NETCore.App/$DOTNET_VERSION/System.Runtime.dll /app/publish/dlls/
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish /app/
|
||||
|
||||
RUN apk add --no-cache icu-libs tzdata ca-certificates
|
||||
|
||||
RUN echo '#!/bin/sh' >> ./start.sh
|
||||
RUN echo 'update-ca-certificates' >> ./start.sh
|
||||
RUN echo 'cd /app' >> ./start.sh
|
||||
RUN echo 'exec ./RobotNet.ScriptManager' >> ./start.sh
|
||||
|
||||
RUN chmod +x ./RobotNet.ScriptManager
|
||||
RUN chmod +x ./start.sh
|
||||
|
||||
# Use the start script to ensure certificates are updated before starting the application
|
||||
EXPOSE 443
|
||||
ENTRYPOINT ["./start.sh"]
|
||||
707
RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs
Normal file
707
RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs
Normal file
@@ -0,0 +1,707 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.ScriptManager.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace RobotNet.ScriptManager.Helpers;
|
||||
|
||||
public static class CSharpSyntaxHelper
|
||||
{
|
||||
public static bool ResolveValueFromString(string valueStr, Type type, out object? value)
|
||||
{
|
||||
// Check if type is in MissionParameterTypes
|
||||
if (!MissionParameterTypes.Contains(type))
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If type is RobotNet.Script.IRobot, return null
|
||||
if (type == RobotType)
|
||||
{
|
||||
value = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert string to the corresponding type
|
||||
if (type == typeof(string))
|
||||
{
|
||||
value = valueStr;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
value = Enum.Parse(type, valueStr, ignoreCase: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle nullable types
|
||||
var underlyingType = Nullable.GetUnderlyingType(type);
|
||||
if (underlyingType != null)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = Convert.ChangeType(valueStr, type);
|
||||
if (value is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public static bool VerifyScript(string code, out string error,
|
||||
out List<ScriptVariableSyntax> variables,
|
||||
out List<ScriptTaskData> tasks,
|
||||
out List<ScriptMissionData> missions)
|
||||
{
|
||||
try
|
||||
{
|
||||
var listVariables = new List<ScriptVariableSyntax>();
|
||||
var wrappedCode = string.Join(Environment.NewLine, [ScriptConfiguration.UsingNamespacesScript, "public class DummyClass", "{", ScriptConfiguration.DeveloptGlobalsScript, code, "}"]);
|
||||
|
||||
var devCompilation = CSharpCompilation.Create("CodeAnalysis")
|
||||
.WithReferences(ScriptConfiguration.MetadataReferences)
|
||||
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
|
||||
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(wrappedCode));
|
||||
|
||||
var diagnostics = devCompilation.GetDiagnostics();
|
||||
if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))
|
||||
{
|
||||
var message = "Verify errors: \r\n";
|
||||
foreach (var diag in diagnostics)
|
||||
{
|
||||
if (diag.Severity == DiagnosticSeverity.Error)
|
||||
{
|
||||
message += $"\t❌ Error: {diag.GetMessage()} at {diag.Location}\r\n";
|
||||
}
|
||||
else if (diag.Severity == DiagnosticSeverity.Warning)
|
||||
{
|
||||
message += $"\t⚠️ Warning: {diag.GetMessage()} at {diag.Location}\r\n";
|
||||
}
|
||||
}
|
||||
throw new Exception(message);
|
||||
}
|
||||
|
||||
error = "";
|
||||
wrappedCode = string.Join(Environment.NewLine, [ScriptConfiguration.UsingNamespacesScript, "public class DummyClass", "{", code, "}"]);
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(wrappedCode);
|
||||
var root = syntaxTree.GetCompilationUnitRoot();
|
||||
|
||||
var runCompilation = CSharpCompilation.Create("CodeAnalysis")
|
||||
.AddSyntaxTrees(syntaxTree)
|
||||
.WithReferences(ScriptConfiguration.MetadataReferences)
|
||||
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
var classNode = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault(c => c.Identifier.Text.Equals("DummyClass")) ?? throw new Exception("No class named 'DummyClass' found in the script.");
|
||||
var semanticModel = runCompilation.GetSemanticModel(syntaxTree);
|
||||
GetScriptVariables(classNode, semanticModel, out variables);
|
||||
GetScriptTasksAndMissions(classNode, semanticModel, out tasks, out missions);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"An error occurred while verifying the script: {ex.Message}";
|
||||
variables = [];
|
||||
tasks = [];
|
||||
missions = [];
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
GC.Collect();
|
||||
}
|
||||
}
|
||||
|
||||
private static void GetScriptVariables(ClassDeclarationSyntax classNode, SemanticModel semanticModel, out List<ScriptVariableSyntax> variables)
|
||||
{
|
||||
variables = [];
|
||||
var fields = classNode.Members.OfType<FieldDeclarationSyntax>();
|
||||
foreach (var field in fields)
|
||||
{
|
||||
Type resolvedType = semanticModel.ToSystemType(field.Declaration.Type) ?? throw new Exception($"Failed to resolve type for field: {field.Declaration.Type.ToFullString()}");
|
||||
// Check if the field has VariableAttribute
|
||||
|
||||
VariableAttribute? varAttr = null;
|
||||
foreach (var attrList in field.AttributeLists)
|
||||
{
|
||||
foreach (var attr in attrList.Attributes)
|
||||
{
|
||||
var attrType = semanticModel.GetTypeAttribute(attr);
|
||||
if (attrType == VariableAttributeType)
|
||||
{
|
||||
varAttr = semanticModel.GetConstantAttribute(attr, attrType) as RobotNet.Script.VariableAttribute;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (varAttr != null) break;
|
||||
}
|
||||
|
||||
foreach (var variable in field.Declaration.Variables)
|
||||
{
|
||||
var name = variable.Identifier.Text;
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
|
||||
|
||||
|
||||
if (variable.Initializer is null)
|
||||
{
|
||||
var value = resolvedType.IsValueType ? Activator.CreateInstance(resolvedType) : null;
|
||||
variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false));
|
||||
}
|
||||
else
|
||||
{
|
||||
var constant = semanticModel.GetConstantValue(variable.Initializer.Value);
|
||||
if (constant.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = Convert.ChangeType(constant.Value, resolvedType);
|
||||
variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to convert value of {name} = \"{constant.Value}\" to {resolvedType}: {ex}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var code = variable.Initializer.Value.ToFullString();
|
||||
object? value;
|
||||
if (string.IsNullOrEmpty(code))
|
||||
{
|
||||
value = resolvedType.IsValueType ? Activator.CreateInstance(resolvedType) : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = CSharpScript.EvaluateAsync<object>(code, ScriptConfiguration.ScriptOptions).GetAwaiter().GetResult();
|
||||
}
|
||||
variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: kiểm tra các properties là auto-property có đủ và getter và setter thì add vào variables
|
||||
var properties = classNode.Members.OfType<PropertyDeclarationSyntax>();
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
// Kiểm tra có cả getter và setter
|
||||
var accessors = prop.AccessorList?.Accessors.ToList() ?? [];
|
||||
bool hasGetter = accessors?.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration) == true;
|
||||
bool hasSetter = accessors?.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration) == true;
|
||||
|
||||
// Kiểm tra auto-property: cả getter và setter đều không có body và không phải expression-bodied
|
||||
bool isAutoProperty = hasGetter && hasSetter &&
|
||||
accessors!.All(a => a.Body == null && a.ExpressionBody == null) &&
|
||||
prop.ExpressionBody == null;
|
||||
|
||||
if (isAutoProperty)
|
||||
{
|
||||
var name = prop.Identifier.Text;
|
||||
var type = semanticModel.ToSystemType(prop.Type) ?? throw new Exception($"Failed to resolve type for property: {prop.Type.ToFullString()}");
|
||||
// Giá trị mặc định của auto-property là default(T)
|
||||
object? value = type.IsValueType ? Activator.CreateInstance(type) : null;
|
||||
variables.Add(new ScriptVariableSyntax(name, type, value, false, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GetScriptTasksAndMissions(ClassDeclarationSyntax classNode, SemanticModel semanticModel, out List<ScriptTaskData> tasks, out List<ScriptMissionData> missions)
|
||||
{
|
||||
tasks = [];
|
||||
missions = [];
|
||||
var methods = classNode.Members.OfType<MethodDeclarationSyntax>();
|
||||
foreach (var method in methods)
|
||||
{
|
||||
bool attrDone = false;
|
||||
foreach (var attrList in method.AttributeLists)
|
||||
{
|
||||
foreach (var attr in attrList.Attributes)
|
||||
{
|
||||
var attrType = semanticModel.GetTypeAttribute(attr);
|
||||
if (attrType == TaskAttributeType)
|
||||
{
|
||||
attrDone = true;
|
||||
|
||||
if (semanticModel.GetConstantAttribute(attr, attrType) is not RobotNet.Script.TaskAttribute taskAttr)
|
||||
{
|
||||
throw new Exception($"Failed to get TaskAttribute from method {method.Identifier.Text}");
|
||||
}
|
||||
|
||||
// Check if method returns Task or Task<T>
|
||||
var returnType = semanticModel.ToSystemType(method.ReturnType);
|
||||
bool isTask = returnType == typeof(System.Threading.Tasks.Task) ||
|
||||
(returnType != null && returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(System.Threading.Tasks.Task<>));
|
||||
bool isVoid = returnType == typeof(void);
|
||||
|
||||
if (method.ParameterList.Parameters.Count > 0 || !(isTask || isVoid))
|
||||
{
|
||||
throw new Exception($"Task Method {method.Identifier.Text} with TaskAttribute must have no parameters and return type void or Task.");
|
||||
}
|
||||
|
||||
tasks.Add(new ScriptTaskData(method.Identifier.Text,
|
||||
taskAttr.Interval,
|
||||
taskAttr.AutoStart,
|
||||
method.ToFullString(),
|
||||
ExtractRelatedCodeForScriptRunner(classNode, method, $"{(isTask ? "await " : "")}{method.Identifier.Text}();")));
|
||||
break;
|
||||
}
|
||||
else if (attrType == MisionAttributeType)
|
||||
{
|
||||
attrDone = true;
|
||||
if (semanticModel.GetConstantAttribute(attr, attrType) is not RobotNet.Script.MissionAttribute missionAttr)
|
||||
{
|
||||
throw new Exception($"Failed to get MissionAttribute from method {method.Identifier.Text}");
|
||||
}
|
||||
var returnType = semanticModel.ToSystemType(method.ReturnType);
|
||||
if (returnType is null || returnType != MisionReturnType)
|
||||
{
|
||||
throw new Exception($"Mission Method {method.Identifier.Text} with MissionAttribute must return type IEnumerator<MissionState>.");
|
||||
}
|
||||
|
||||
var inputParameters = new List<string>();
|
||||
var parameters = new List<ScriptMissionParameter>();
|
||||
bool hasCancellationTokenParameter = false;
|
||||
foreach (var param in method.ParameterList.Parameters)
|
||||
{
|
||||
if (param.Type is null)
|
||||
{
|
||||
throw new Exception($"Parameter {param.Identifier.Text} in method {method.Identifier.Text} has no type specified.");
|
||||
}
|
||||
|
||||
var paramType = semanticModel.ToSystemType(param.Type) ?? throw new Exception($"Failed to resolve type for parameter {param.Identifier.Text} in method {method.Identifier.Text}");
|
||||
if (!MissionParameterTypes.Contains(paramType))
|
||||
{
|
||||
throw new Exception($"Parameter type {param.Type} {param.Identifier.Text} in method mission {method.Identifier.Text} is not supported");
|
||||
}
|
||||
|
||||
if (paramType == typeof(CancellationToken))
|
||||
{
|
||||
if (hasCancellationTokenParameter)
|
||||
{
|
||||
throw new Exception($"Method {method.Identifier.Text} has multiple CancellationToken parameters, which is not allowed.");
|
||||
}
|
||||
hasCancellationTokenParameter = true;
|
||||
}
|
||||
|
||||
// lấy default value nếu có
|
||||
object? defaultValue = null;
|
||||
if (param.Default is EqualsValueClauseSyntax equalsValue)
|
||||
{
|
||||
var constValue = semanticModel.GetConstantValue(equalsValue.Value);
|
||||
if (constValue.HasValue)
|
||||
{
|
||||
defaultValue = constValue.Value;
|
||||
}
|
||||
}
|
||||
|
||||
//inputParameters.Add($@"({paramType.FullName})parameters["""+param.Identifier.Text+"""]");
|
||||
inputParameters.Add($@"({paramType.FullName})parameters[""{param.Identifier.Text}""]");
|
||||
parameters.Add(new ScriptMissionParameter(param.Identifier.Text, paramType, defaultValue));
|
||||
}
|
||||
|
||||
var execScript = $"return {method.Identifier.Text}({string.Join(", ", inputParameters)});";
|
||||
missions.Add(new ScriptMissionData(method.Identifier.Text,
|
||||
parameters,
|
||||
method.ToFullString(),
|
||||
ExtractRelatedCodeForScriptRunner(classNode, method, execScript),
|
||||
missionAttr.IsMultipleRun));
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if (attrDone) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractRelatedCodeForScriptRunner(ClassDeclarationSyntax classNode, MethodDeclarationSyntax rootMethod, string execScript)
|
||||
{
|
||||
var allMethods = classNode.Members.OfType<MethodDeclarationSyntax>().ToList();
|
||||
var allFields = classNode.Members.OfType<FieldDeclarationSyntax>().ToList();
|
||||
var allProperties = classNode.Members.OfType<PropertyDeclarationSyntax>().ToList();
|
||||
var allNestedTypes = classNode.Members
|
||||
.Where(m => m is ClassDeclarationSyntax || m is StructDeclarationSyntax || m is InterfaceDeclarationSyntax || m is EnumDeclarationSyntax)
|
||||
.ToList();
|
||||
|
||||
// 1. BFS: method + non-auto-property
|
||||
var usedMethodNames = new HashSet<string>();
|
||||
var usedPropertyNames = new HashSet<string>();
|
||||
var methodQueue = new Queue<MemberDeclarationSyntax>();
|
||||
var collectedMethods = new List<MethodDeclarationSyntax>();
|
||||
var collectedNonAutoProperties = new List<PropertyDeclarationSyntax>();
|
||||
|
||||
methodQueue.Enqueue(rootMethod);
|
||||
|
||||
while (methodQueue.Count > 0)
|
||||
{
|
||||
var member = methodQueue.Dequeue();
|
||||
if (member is MethodDeclarationSyntax method)
|
||||
{
|
||||
if (!usedMethodNames.Add(method.Identifier.Text))
|
||||
continue;
|
||||
collectedMethods.Add(method);
|
||||
|
||||
// Tìm các method/property được gọi trong method này
|
||||
var invokedNames = method.DescendantNodes()
|
||||
.OfType<IdentifierNameSyntax>()
|
||||
.Select(id => id.Identifier.Text)
|
||||
.Distinct();
|
||||
|
||||
foreach (var name in invokedNames)
|
||||
{
|
||||
// Method
|
||||
var nextMethod = allMethods.FirstOrDefault(m => m.Identifier.Text == name);
|
||||
if (nextMethod != null && !usedMethodNames.Contains(name))
|
||||
methodQueue.Enqueue(nextMethod);
|
||||
|
||||
// Property
|
||||
var nextProp = allProperties.FirstOrDefault(p => p.Identifier.Text == name);
|
||||
if (nextProp != null && !usedPropertyNames.Contains(name))
|
||||
methodQueue.Enqueue(nextProp);
|
||||
}
|
||||
}
|
||||
else if (member is PropertyDeclarationSyntax prop)
|
||||
{
|
||||
if (!usedPropertyNames.Add(prop.Identifier.Text))
|
||||
continue;
|
||||
|
||||
// Auto-property: bỏ qua, sẽ xử lý sau
|
||||
var accessors = prop.AccessorList?.Accessors.ToList() ?? [];
|
||||
bool hasGetter = accessors.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration);
|
||||
bool hasSetter = accessors.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration);
|
||||
bool isAutoProperty = hasGetter && hasSetter &&
|
||||
accessors.All(a => a.Body == null && a.ExpressionBody == null) &&
|
||||
prop.ExpressionBody == null;
|
||||
|
||||
if (isAutoProperty)
|
||||
continue;
|
||||
|
||||
collectedNonAutoProperties.Add(prop);
|
||||
|
||||
// Tìm các method/property/field được gọi trong property này
|
||||
var invokedNames = prop.DescendantNodes()
|
||||
.OfType<IdentifierNameSyntax>()
|
||||
.Select(id => id.Identifier.Text)
|
||||
.Distinct();
|
||||
|
||||
foreach (var name in invokedNames)
|
||||
{
|
||||
// Method
|
||||
var nextMethod = allMethods.FirstOrDefault(m => m.Identifier.Text == name);
|
||||
if (nextMethod != null && !usedMethodNames.Contains(name))
|
||||
methodQueue.Enqueue(nextMethod);
|
||||
|
||||
// Property
|
||||
var nextProp = allProperties.FirstOrDefault(p => p.Identifier.Text == name);
|
||||
if (nextProp != null && !usedPropertyNames.Contains(name))
|
||||
methodQueue.Enqueue(nextProp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Collect all used member names (from all collected methods & non-auto-properties)
|
||||
var usedMemberNames = new HashSet<string>();
|
||||
foreach (var method in collectedMethods)
|
||||
{
|
||||
foreach (var id in method.DescendantNodes().OfType<IdentifierNameSyntax>().Select(id => id.Identifier.Text))
|
||||
usedMemberNames.Add(id);
|
||||
}
|
||||
foreach (var prop in collectedNonAutoProperties)
|
||||
{
|
||||
foreach (var id in prop.DescendantNodes().OfType<IdentifierNameSyntax>().Select(id => id.Identifier.Text))
|
||||
usedMemberNames.Add(id);
|
||||
}
|
||||
|
||||
// 3. Collect fields
|
||||
var relatedFields = new List<string>();
|
||||
foreach (var field in allFields)
|
||||
{
|
||||
foreach (var variable in field.Declaration.Variables)
|
||||
{
|
||||
if (usedMemberNames.Contains(variable.Identifier.Text))
|
||||
{
|
||||
var varType = field.Declaration.Type.ToString();
|
||||
var varName = variable.Identifier.Text;
|
||||
var propertyCode = $@"public {varType} {varName}
|
||||
{{
|
||||
get => ({varType})globals[""{varName}""];
|
||||
set => globals[""{varName}""] = value;
|
||||
}}";
|
||||
relatedFields.Add(propertyCode.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Collect auto-properties
|
||||
var relatedAutoProperties = new List<string>();
|
||||
foreach (var prop in allProperties)
|
||||
{
|
||||
var accessors = prop.AccessorList?.Accessors.ToList() ?? [];
|
||||
bool hasGetter = accessors.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration);
|
||||
bool hasSetter = accessors.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration);
|
||||
bool isAutoProperty = hasGetter && hasSetter &&
|
||||
accessors.All(a => a.Body == null && a.ExpressionBody == null) &&
|
||||
prop.ExpressionBody == null;
|
||||
|
||||
if (isAutoProperty && (usedMemberNames.Contains(prop.Identifier.Text) || usedPropertyNames.Contains(prop.Identifier.Text)))
|
||||
{
|
||||
var propType = prop.Type.ToString();
|
||||
var propName = prop.Identifier.Text;
|
||||
var propertyCode = $@"public {propType} {propName}
|
||||
{{
|
||||
get => ({propType})globals[""{propName}""];
|
||||
set => globals[""{propName}""] = value;
|
||||
}}";
|
||||
relatedAutoProperties.Add(propertyCode.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Collect nested types if referenced
|
||||
var usedNestedTypeNames = new HashSet<string>(usedMemberNames);
|
||||
var relatedNestedTypes = allNestedTypes
|
||||
.Where(nt =>
|
||||
{
|
||||
if (nt is BaseTypeDeclarationSyntax btd)
|
||||
return usedNestedTypeNames.Contains(btd.Identifier.Text);
|
||||
return false;
|
||||
})
|
||||
.Select(nt => nt.NormalizeWhitespace().ToFullString())
|
||||
.ToList();
|
||||
|
||||
// 6. Compose the script
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(ScriptConfiguration.RuntimeGlobalsScript);
|
||||
foreach (var nt in relatedNestedTypes)
|
||||
sb.AppendLine(nt);
|
||||
foreach (var f in relatedFields)
|
||||
sb.AppendLine(f);
|
||||
foreach (var p in relatedAutoProperties)
|
||||
sb.AppendLine(p);
|
||||
foreach (var p in collectedNonAutoProperties)
|
||||
sb.AppendLine(p.NormalizeWhitespace().ToFullString());
|
||||
foreach (var m in collectedMethods)
|
||||
sb.AppendLine(m.NormalizeWhitespace().ToFullString());
|
||||
|
||||
sb.AppendLine(execScript);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static Type? GetTypeAttribute(this SemanticModel semanticModel, AttributeSyntax attrSynctax)
|
||||
{
|
||||
var typeInfo = semanticModel.GetTypeInfo(attrSynctax);
|
||||
if (typeInfo.Type is null) return null;
|
||||
|
||||
string metadataName = typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
|
||||
return ResolveTypeFromString(metadataName);
|
||||
}
|
||||
|
||||
private static object? GetConstantAttribute(this SemanticModel semanticModel, AttributeSyntax attrSynctax, Type type)
|
||||
{
|
||||
var args = new List<object?>();
|
||||
|
||||
foreach (var arg in attrSynctax.ArgumentList?.Arguments ?? default)
|
||||
{
|
||||
var constValue = semanticModel.GetConstantValue(arg.Expression);
|
||||
if (constValue.HasValue)
|
||||
args.Add(constValue.Value);
|
||||
else
|
||||
args.Add(null); // fallback nếu không phân giải được
|
||||
}
|
||||
|
||||
// Find the constructor with the same number or more parameters (with optional)
|
||||
var ctors = type.GetConstructors();
|
||||
foreach (var ctor in ctors)
|
||||
{
|
||||
var parameters = ctor.GetParameters();
|
||||
if (args.Count <= parameters.Length)
|
||||
{
|
||||
// Fill missing optional parameters with their default values
|
||||
var finalArgs = args.ToList();
|
||||
for (int i = args.Count; i < parameters.Length; i++)
|
||||
{
|
||||
if (parameters[i].IsOptional)
|
||||
finalArgs.Add(parameters[i].DefaultValue);
|
||||
else
|
||||
goto NextCtor; // Not enough arguments and not optional
|
||||
}
|
||||
return ctor.Invoke([.. finalArgs]);
|
||||
}
|
||||
NextCtor:;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Type? ToSystemType(this SemanticModel semanticModel, TypeSyntax typeSyntax)
|
||||
{
|
||||
var typeSymbol = semanticModel.GetTypeInfo(typeSyntax).Type;
|
||||
if (typeSymbol is null) return null;
|
||||
|
||||
if (typeSyntax is PredefinedTypeSyntax predefinedType
|
||||
&& predefinedMap.TryGetValue(predefinedType.Keyword.Text, out var systemType))
|
||||
{
|
||||
return systemType;
|
||||
}
|
||||
|
||||
string metadataName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
|
||||
return metadataName.Equals("void") ? typeof(void) : ResolveTypeFromString(metadataName);
|
||||
}
|
||||
|
||||
private static Type? ResolveTypeFromString(string typeString)
|
||||
{
|
||||
typeString = typeString.Trim();
|
||||
|
||||
// Handle array types (e.g., System.Int32[], string[], List<int>[])
|
||||
if (typeString.EndsWith("[]"))
|
||||
{
|
||||
var elementTypeString = typeString[..^2].Trim();
|
||||
var elementType = ResolveTypeFromString(elementTypeString);
|
||||
return elementType?.MakeArrayType();
|
||||
}
|
||||
|
||||
// Trường hợp không phải generic
|
||||
if (!typeString.Contains('<'))
|
||||
{
|
||||
return FindType(typeString);
|
||||
}
|
||||
|
||||
// Tách phần generic
|
||||
var match = Regex.Match(typeString, @"^(?<raw>[^<]+)<(?<args>.+)>$");
|
||||
if (!match.Success)
|
||||
return null;
|
||||
|
||||
var genericTypeName = match.Groups["raw"].Value;
|
||||
var genericArgsString = match.Groups["args"].Value;
|
||||
|
||||
// Phân tách các generic argument (xử lý nested generics)
|
||||
var genericArgs = SplitGenericArguments(genericArgsString);
|
||||
|
||||
var genericType = FindType(genericTypeName + "`" + genericArgs.Count);
|
||||
if (genericType == null)
|
||||
return null;
|
||||
|
||||
var resolvedArgs = genericArgs.Select(ResolveTypeFromString).ToArray();
|
||||
if (resolvedArgs.Any(t => t == null))
|
||||
return null;
|
||||
|
||||
return genericType.MakeGenericType(resolvedArgs!);
|
||||
}
|
||||
|
||||
private static List<string> SplitGenericArguments(string input)
|
||||
{
|
||||
var args = new List<string>();
|
||||
var sb = new StringBuilder();
|
||||
int depth = 0;
|
||||
|
||||
foreach (char c in input)
|
||||
{
|
||||
if (c == ',' && depth == 0)
|
||||
{
|
||||
args.Add(sb.ToString().Trim());
|
||||
sb.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c == '<') depth++;
|
||||
else if (c == '>') depth--;
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
args.Add(sb.ToString().Trim());
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private static Type? FindType(string typeName)
|
||||
{
|
||||
if (predefinedMap.TryGetValue(typeName, out var systemType)) return systemType;
|
||||
|
||||
return AppDomain.CurrentDomain
|
||||
.GetAssemblies()
|
||||
.Select(a => a.GetType(typeName, false))
|
||||
.FirstOrDefault(t => t != null);
|
||||
}
|
||||
|
||||
|
||||
public static string ToTypeString(Type type)
|
||||
{
|
||||
if (type == typeof(void))
|
||||
return "void";
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var genericTypeName = type.GetGenericTypeDefinition().FullName;
|
||||
if (genericTypeName == null)
|
||||
return type.Name;
|
||||
|
||||
var backtickIndex = genericTypeName.IndexOf('`');
|
||||
if (backtickIndex > 0)
|
||||
genericTypeName = genericTypeName.Substring(0, backtickIndex);
|
||||
|
||||
var genericArgs = type.GetGenericArguments();
|
||||
var argsString = string.Join(", ", genericArgs.Select(ToTypeString));
|
||||
return $"{genericTypeName}<{argsString}>";
|
||||
}
|
||||
|
||||
return type.FullName ?? type.Name;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, Type> predefinedMap = new()
|
||||
{
|
||||
["bool"] = typeof(bool),
|
||||
["byte"] = typeof(byte),
|
||||
["sbyte"] = typeof(sbyte),
|
||||
["short"] = typeof(short),
|
||||
["ushort"] = typeof(ushort),
|
||||
["int"] = typeof(int),
|
||||
["uint"] = typeof(uint),
|
||||
["long"] = typeof(long),
|
||||
["ulong"] = typeof(ulong),
|
||||
["float"] = typeof(float),
|
||||
["double"] = typeof(double),
|
||||
["decimal"] = typeof(decimal),
|
||||
["char"] = typeof(char),
|
||||
["string"] = typeof(string),
|
||||
["object"] = typeof(object)
|
||||
};
|
||||
|
||||
private static readonly Type TaskAttributeType = typeof(RobotNet.Script.TaskAttribute);
|
||||
private static readonly Type MisionAttributeType = typeof(RobotNet.Script.MissionAttribute);
|
||||
private static readonly Type MisionReturnType = typeof(IAsyncEnumerable<MissionState>);
|
||||
private static readonly Type VariableAttributeType = typeof(VariableAttribute);
|
||||
|
||||
private static readonly Type RobotType = typeof(RobotNet.Script.IRobot);
|
||||
private static readonly ImmutableArray<Type> MissionParameterTypes = [
|
||||
typeof(bool),
|
||||
typeof(byte),
|
||||
typeof(sbyte),
|
||||
typeof(short),
|
||||
typeof(ushort),
|
||||
typeof(int),
|
||||
typeof(uint),
|
||||
typeof(long),
|
||||
typeof(ulong),
|
||||
typeof(float),
|
||||
typeof(double),
|
||||
typeof(decimal),
|
||||
typeof(char),
|
||||
typeof(string),
|
||||
typeof(CancellationToken),
|
||||
];
|
||||
}
|
||||
226
RobotNet.ScriptManager/Helpers/ScriptConfiguration.cs
Normal file
226
RobotNet.ScriptManager/Helpers/ScriptConfiguration.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using RobotNet.Script.Shares;
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace RobotNet.ScriptManager.Helpers;
|
||||
|
||||
public static class ScriptConfiguration
|
||||
{
|
||||
public static readonly string ScriptStorePath;
|
||||
public static readonly string DllsPath;
|
||||
public static readonly ScriptOptions ScriptOptions;
|
||||
public static readonly ImmutableArray<MetadataReference> MetadataReferences;
|
||||
public static readonly ImmutableArray<string> UsingNamespaces;
|
||||
public static readonly string UsingNamespacesScript;
|
||||
public static readonly string DeveloptGlobalsScript;
|
||||
public static readonly string RuntimeGlobalsScript;
|
||||
|
||||
static ScriptConfiguration()
|
||||
{
|
||||
ScriptStorePath = "scripts";
|
||||
DllsPath = "dlls";
|
||||
UsingNamespaces = ["System", "System.Collections.Generic", "System.Linq.Expressions", "System.Threading", "System.Threading.Tasks", "System.Runtime.CompilerServices", "RobotNet.Script"];
|
||||
UsingNamespacesScript = string.Join(Environment.NewLine, UsingNamespaces.Select(ns => $"using {ns};"));
|
||||
|
||||
List<MetadataReference> metadataRefs = [];
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
if (Directory.Exists(DllsPath))
|
||||
{
|
||||
foreach (var dll in Directory.GetFiles(DllsPath, "*.dll"))
|
||||
{
|
||||
metadataRefs.Add(MetadataReference.CreateFromFile(Path.Combine(currentDirectory, dll), properties: MetadataReferenceProperties.Assembly));
|
||||
}
|
||||
}
|
||||
|
||||
MetadataReferences = [.. metadataRefs];
|
||||
var options = ScriptOptions.Default;
|
||||
options.MetadataReferences.Clear();
|
||||
ScriptOptions = options.AddReferences(MetadataReferences).AddImports(UsingNamespaces).WithEmitDebugInformation(false);
|
||||
|
||||
DeveloptGlobalsScript = BuildDeveloptGlobalsScript();
|
||||
RuntimeGlobalsScript = BuildRuntimeGlobalsScript();
|
||||
}
|
||||
|
||||
public static string GetScriptCode() => ReadAllTextFromFolder(ScriptStorePath);
|
||||
|
||||
private static string ReadAllTextFromFolder(string path)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(path);
|
||||
|
||||
var code = string.Join(Environment.NewLine, [.. dirInfo.GetFiles("*.cs").Select(f => File.ReadAllText(f.FullName))]);
|
||||
var after = string.Join(Environment.NewLine, dirInfo.GetDirectories().Select(dir => ReadAllTextFromFolder(dir.FullName)).ToArray());
|
||||
return string.Join(Environment.NewLine, code, after);
|
||||
}
|
||||
|
||||
private static string BuildRuntimeGlobalsScript()
|
||||
{
|
||||
var type = typeof(IRobotNetGlobals);
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
sb.AppendLine($@"{CSharpSyntaxHelper.ToTypeString(field.FieldType)} {field.Name}
|
||||
{{
|
||||
get => ({CSharpSyntaxHelper.ToTypeString(field.FieldType)})robotnet[""{field.Name}""];
|
||||
set => robotnet[""{field.Name}""] = value;
|
||||
}}");
|
||||
}
|
||||
|
||||
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.GetIndexParameters().Length == 0)
|
||||
{
|
||||
var hasGetter = prop.GetGetMethod() != null;
|
||||
var hasSetter = prop.GetSetMethod() != null;
|
||||
if (hasGetter || hasSetter)
|
||||
{
|
||||
var propBuilder = new StringBuilder();
|
||||
propBuilder.AppendLine($"{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)} {prop.Name}");
|
||||
propBuilder.AppendLine("{");
|
||||
if (hasGetter)
|
||||
propBuilder.AppendLine($@" get => ((Func<{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)}>)robotnet[""get_{prop.Name}""])();");
|
||||
|
||||
if (hasSetter)
|
||||
propBuilder.AppendLine($@" set => ((Action<{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)}>)robotnet[""set_{prop.Name}""])(value);");
|
||||
propBuilder.AppendLine("}");
|
||||
sb.AppendLine(propBuilder.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle indexers (properties with parameters)
|
||||
var indexParams = prop.GetIndexParameters();
|
||||
var paramDecl = string.Join(", ", indexParams.Select(p => $"{CSharpSyntaxHelper.ToTypeString(p.ParameterType)} {p.Name}"));
|
||||
var paramNames = string.Join(", ", indexParams.Select(p => p.Name));
|
||||
var getterDelegateType = $"Func<{string.Join(", ", indexParams.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(prop.PropertyType)]))}>";
|
||||
var setterDelegateType = $"Action<{string.Join(", ", indexParams.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(prop.PropertyType)]))}>";
|
||||
|
||||
var propBuilder = new StringBuilder();
|
||||
propBuilder.AppendLine($"{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)} this[{paramDecl}]");
|
||||
propBuilder.AppendLine("{");
|
||||
if (prop.GetGetMethod() != null)
|
||||
propBuilder.AppendLine($@" get => (({getterDelegateType})robotnet[""get_{prop.Name}_indexer""])({paramNames});");
|
||||
if (prop.GetSetMethod() != null)
|
||||
propBuilder.AppendLine($@" set => (({setterDelegateType})robotnet[""set_{prop.Name}_indexer""])({(string.IsNullOrEmpty(paramNames) ? "value" : paramNames + ", value")});");
|
||||
propBuilder.AppendLine("}");
|
||||
sb.AppendLine(propBuilder.ToString());
|
||||
}
|
||||
}
|
||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (!method.IsSpecialName)
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
var parametersStr = string.Join(", ", parameters.Select(ToParameterString));
|
||||
var args = string.Join(", ", parameters.Select(p => p.Name));
|
||||
var returnType = CSharpSyntaxHelper.ToTypeString(method.ReturnType);
|
||||
|
||||
|
||||
var paramTypes = string.Join(",", parameters.Select(p => p.ParameterType.FullName));
|
||||
var methodKey = $"{method.Name}({paramTypes})";
|
||||
|
||||
var methodType = method.ReturnType == typeof(void)
|
||||
? (parameters.Length == 0 ? "Action" : $"Action<{string.Join(", ", parameters.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)))}>")
|
||||
: $"Func<{string.Join(", ", parameters.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(method.ReturnType)]))}>";
|
||||
|
||||
sb.AppendLine($@"{returnType} {method.Name}({parametersStr}) => (({methodType})robotnet[""{methodKey}""]){(string.IsNullOrEmpty(args) ? "()" : $"({args})")};");
|
||||
}
|
||||
}
|
||||
|
||||
var code = sb.ToString();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string BuildDeveloptGlobalsScript()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var glovalType = typeof(IRobotNetGlobals);
|
||||
|
||||
var properties = glovalType.GetProperties();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propStr = "";
|
||||
if (property.CanRead)
|
||||
{
|
||||
propStr = $"{CSharpSyntaxHelper.ToTypeString(property.PropertyType)} {property.Name} {{ get; {(property.CanWrite ? "set; " : "")}}}";
|
||||
}
|
||||
else if (property.CanWrite)
|
||||
{
|
||||
propStr = $"{CSharpSyntaxHelper.ToTypeString(property.PropertyType)} {property.Name} {{ set => throw new System.NotImplementedException(); }}";
|
||||
}
|
||||
if(string.IsNullOrEmpty(propStr)) continue;
|
||||
sb.AppendLine(propStr);
|
||||
}
|
||||
|
||||
var fields = glovalType.GetFields();
|
||||
foreach (var field in fields)
|
||||
{
|
||||
sb.AppendLine($"{CSharpSyntaxHelper.ToTypeString(field.FieldType)} {field.Name};");
|
||||
}
|
||||
|
||||
var methods = glovalType.GetMethods();
|
||||
foreach (var method in methods)
|
||||
{
|
||||
if (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")) continue;
|
||||
|
||||
var notImplementedMethod = $"{CSharpSyntaxHelper.ToTypeString(method.ReturnType)} {method.Name}({string.Join(',', method.GetParameters().Select(ToParameterString))}) => throw new System.NotImplementedException();";
|
||||
sb.AppendLine(notImplementedMethod);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ToParameterString(ParameterInfo parameter)
|
||||
{
|
||||
var modifier = "";
|
||||
if (parameter.IsDefined(typeof(ParamArrayAttribute), false))
|
||||
modifier = "params ";
|
||||
else if (parameter.IsIn && parameter.ParameterType.IsByRef && !parameter.IsOut)
|
||||
modifier = "in ";
|
||||
else if (parameter.IsOut)
|
||||
modifier = "out ";
|
||||
else if (parameter.ParameterType.IsByRef)
|
||||
modifier = "ref ";
|
||||
|
||||
var typeString = CSharpSyntaxHelper.ToTypeString(
|
||||
parameter.ParameterType.IsByRef
|
||||
? parameter.ParameterType.GetElementType()!
|
||||
: parameter.ParameterType
|
||||
);
|
||||
|
||||
var defaultValue = "";
|
||||
if (parameter.HasDefaultValue)
|
||||
{
|
||||
if (parameter.DefaultValue != null)
|
||||
{
|
||||
if (parameter.ParameterType.IsEnum)
|
||||
{
|
||||
defaultValue = $" = {CSharpSyntaxHelper.ToTypeString(parameter.ParameterType)}.{parameter.DefaultValue}";
|
||||
}
|
||||
else if (parameter.DefaultValue is string)
|
||||
{
|
||||
defaultValue = $" = \"{parameter.DefaultValue}\"";
|
||||
}
|
||||
else if (parameter.DefaultValue is bool b)
|
||||
{
|
||||
defaultValue = $" = {b.ToString().ToLower()}";
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultValue = $" = {parameter.DefaultValue}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultValue = " = null";
|
||||
}
|
||||
}
|
||||
|
||||
return $"{modifier}{typeString} {parameter.Name}{defaultValue}";
|
||||
}
|
||||
|
||||
}
|
||||
74
RobotNet.ScriptManager/Helpers/VDA5050Helper.cs
Normal file
74
RobotNet.ScriptManager/Helpers/VDA5050Helper.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using RobotNet.RobotShares.Models;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Expressions;
|
||||
|
||||
namespace RobotNet.ScriptManager.Helpers;
|
||||
|
||||
public static class VDA5050ScriptHelper
|
||||
{
|
||||
public static RobotState ConvertToRobotState(RobotStateModel model)
|
||||
{
|
||||
bool isReady = model.IsOnline && !model.OrderState.IsProcessing && model.Errors.Length == 0;
|
||||
if(model.ActionStates.Length > 0)
|
||||
{
|
||||
isReady = isReady && model.ActionStates.All(a => !a.IsProcessing);
|
||||
}
|
||||
return new RobotState(isReady, model.BatteryState.BatteryVoltage, model.Loads.Length != 0, model.BatteryState.Charging, model.AgvPosition.X, model.AgvPosition.Y, model.AgvPosition.Theta);
|
||||
}
|
||||
|
||||
public static RobotInstantActionModel ConvertToRobotInstantActionModel(string robotId, RobotAction action)
|
||||
{
|
||||
return new RobotInstantActionModel
|
||||
{
|
||||
RobotId = robotId,
|
||||
Action = new RobotShares.VDA5050.InstantAction.Action
|
||||
{
|
||||
ActionType = action.ActionType,
|
||||
BlockingType = action.BlockingType switch
|
||||
{
|
||||
BlockingType.NONE => "NONE",
|
||||
BlockingType.SOFT => "SOFT",
|
||||
BlockingType.HARD => "HARD",
|
||||
_ => "NONE"
|
||||
},
|
||||
ActionParameters = [..action.Parameters?.Select(p => new RobotShares.VDA5050.InstantAction.ActionParameter
|
||||
{
|
||||
Key = p.Key,
|
||||
Value = p.Value
|
||||
}) ?? []],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static RobotShares.Models.RobotMoveToNodeModel ConvertToRobotMoveToNodeModel(string robotId, string nodeName, IDictionary<string, IEnumerable<RobotAction>> actions, double? lastAngle)
|
||||
{
|
||||
return new RobotShares.Models.RobotMoveToNodeModel
|
||||
{
|
||||
RobotId = robotId,
|
||||
NodeName = nodeName,
|
||||
LastAngle = lastAngle ?? 0,
|
||||
OverrideLastAngle = lastAngle != null,
|
||||
Actions = actions.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.Select(a => new RobotShares.VDA5050.InstantAction.Action
|
||||
{
|
||||
ActionType = a.ActionType,
|
||||
ActionId = Guid.NewGuid().ToString(),
|
||||
BlockingType = a.BlockingType switch
|
||||
{
|
||||
BlockingType.NONE => "NONE",
|
||||
BlockingType.SOFT => "SOFT",
|
||||
BlockingType.HARD => "HARD",
|
||||
_ => "NONE"
|
||||
},
|
||||
ActionParameters = [..a.Parameters?.Select(p => new RobotShares.VDA5050.InstantAction.ActionParameter
|
||||
{
|
||||
Key = p.Key,
|
||||
Value = p.Value
|
||||
}) ?? []],
|
||||
})
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
20
RobotNet.ScriptManager/Hubs/ConsoleHub.cs
Normal file
20
RobotNet.ScriptManager/Hubs/ConsoleHub.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace RobotNet.ScriptManager.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class ConsoleHub : Hub
|
||||
{
|
||||
public Task RegisterTasksConsole(string name) => Groups.AddToGroupAsync(Context.ConnectionId, $"task-{name}");
|
||||
public Task UnregisterTasksConsole(string name) => Groups.RemoveFromGroupAsync(Context.ConnectionId, $"task-{name}");
|
||||
public Task RegisterTaskConsoles() => Groups.AddToGroupAsync(Context.ConnectionId, "tasks");
|
||||
public Task UnregisterTaskConsoles() => Groups.RemoveFromGroupAsync(Context.ConnectionId, "tasks");
|
||||
public Task RegisterMissionConsole(Guid missionId) => Groups.AddToGroupAsync(Context.ConnectionId, $"mission-{missionId}");
|
||||
public Task UnregisterMissionConsole(Guid missionId) => Groups.RemoveFromGroupAsync(Context.ConnectionId, $"mission-{missionId}");
|
||||
public Task RegisterMissionConsoles() => Groups.AddToGroupAsync(Context.ConnectionId, "missions");
|
||||
public Task UnregisterMissionConsoles() => Groups.RemoveFromGroupAsync(Context.ConnectionId, "missions");
|
||||
public Task RegisterAllConsoles() => Groups.AddToGroupAsync(Context.ConnectionId, "alls");
|
||||
public Task UnregisterAllConsoles() => Groups.RemoveFromGroupAsync(Context.ConnectionId, "alls");
|
||||
|
||||
}
|
||||
24
RobotNet.ScriptManager/Hubs/DashboardHub.cs
Normal file
24
RobotNet.ScriptManager/Hubs/DashboardHub.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares.Dashboard;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class DashboardHub(DashboardPublisher DashboardPublisher, ILogger<DashboardHub> Logger) : Hub
|
||||
{
|
||||
public MessageResult<DashboardDto> GetDashboardData()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new(true, "") { Data = DashboardPublisher.GetData() };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning("Lấy dữ liệu Dashboard xảy ra lỗi: {ex}", ex.Message);
|
||||
return new(false, "Lấy dữ liệu Dashboard xảy ra lỗi");
|
||||
}
|
||||
}
|
||||
}
|
||||
45
RobotNet.ScriptManager/Hubs/HMIHub.cs
Normal file
45
RobotNet.ScriptManager/Hubs/HMIHub.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class HMIHub(ScriptStateManager scriptBuilder, ScriptGlobalsManager globalsManager) : Hub
|
||||
{
|
||||
public ProcessorState GetState() => scriptBuilder.State;
|
||||
|
||||
public ProcessorRequest GetRequest() => scriptBuilder.Request;
|
||||
|
||||
public IDictionary<string, string> GetVariables(IEnumerable<string> keys)
|
||||
{
|
||||
var variables = new Dictionary<string, string>();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (globalsManager.Globals.TryGetValue(key, out object? val))
|
||||
{
|
||||
variables.Add(key, val?.ToString() ?? "null");
|
||||
}
|
||||
else
|
||||
{
|
||||
variables.Add(key, "null");
|
||||
}
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
public MessageResult SetVariable(string key, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
globalsManager.SetValue(key, value);
|
||||
return new(true, "");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(false, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
RobotNet.ScriptManager/Hubs/ProcessorHub.cs
Normal file
47
RobotNet.ScriptManager/Hubs/ProcessorHub.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class ProcessorHub(ScriptStateManager scriptBuilder) : Hub
|
||||
{
|
||||
public ProcessorState GetState() => scriptBuilder.State;
|
||||
|
||||
public ProcessorRequest GetRequest() => scriptBuilder.Request;
|
||||
|
||||
public MessageResult Build()
|
||||
{
|
||||
var message = "";
|
||||
var result = scriptBuilder.Build(ref message);
|
||||
|
||||
return new(result, message);
|
||||
}
|
||||
|
||||
public MessageResult Run()
|
||||
{
|
||||
var message = "";
|
||||
var result = scriptBuilder.Run(ref message);
|
||||
|
||||
return new(result, message);
|
||||
}
|
||||
|
||||
public MessageResult Stop()
|
||||
{
|
||||
var message = "";
|
||||
var result = scriptBuilder.Stop(ref message);
|
||||
|
||||
return new(result, message);
|
||||
}
|
||||
|
||||
public MessageResult Reset()
|
||||
{
|
||||
var message = "";
|
||||
var result = scriptBuilder.Reset(ref message);
|
||||
|
||||
return new(result, message);
|
||||
}
|
||||
}
|
||||
68
RobotNet.ScriptManager/Hubs/ScriptOpenHub.cs
Normal file
68
RobotNet.ScriptManager/Hubs/ScriptOpenHub.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class ScriptOpenHub(ScriptStateManager scriptBuilder, ScriptGlobalsManager globalManager) : Hub
|
||||
{
|
||||
public ProcessorState GetState() => scriptBuilder.State;
|
||||
|
||||
public MessageResult Run()
|
||||
{
|
||||
var message = "";
|
||||
var result = scriptBuilder.Run(ref message);
|
||||
|
||||
return new(result, message);
|
||||
}
|
||||
|
||||
public MessageResult Stop()
|
||||
{
|
||||
var message = "";
|
||||
var result = scriptBuilder.Stop(ref message);
|
||||
|
||||
return new(result, message);
|
||||
}
|
||||
|
||||
public IDictionary<string, string> GetVariables() => globalManager.GetVariablesData().ToDictionary(v => v.Name, v => v.Value);
|
||||
|
||||
public string GetVariableValue(string name)
|
||||
{
|
||||
if (globalManager.Globals.TryGetValue(name, out var value))
|
||||
{
|
||||
return value?.ToString() ?? "null";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetVariableValue(string name, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
globalManager.SetValue(name, value);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public bool ResetVariableValue(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
globalManager.ResetValue(name);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
RobotNet.ScriptManager/Hubs/ScriptTaskHub.cs
Normal file
33
RobotNet.ScriptManager/Hubs/ScriptTaskHub.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using RobotNet.Shares;
|
||||
|
||||
namespace RobotNet.ScriptManager.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class ScriptTaskHub(ScriptTaskManager taskManager) : Hub
|
||||
{
|
||||
public MessageResult Pause(string name)
|
||||
{
|
||||
if (taskManager.Pause(name))
|
||||
{
|
||||
return new MessageResult(true, $"Task '{name}' paused successfully.");
|
||||
}
|
||||
return new MessageResult(false, $"Task '{name}' not found or could not be paused.");
|
||||
}
|
||||
|
||||
public MessageResult Resume(string name)
|
||||
{
|
||||
if (taskManager.Resume(name))
|
||||
{
|
||||
return new MessageResult(true, $"Task '{name}' resumed successfully.");
|
||||
}
|
||||
return new MessageResult(false, $"Task '{name}' not found or could not be resumed.");
|
||||
}
|
||||
|
||||
public IDictionary<string, bool> GetTaskStates()
|
||||
{
|
||||
return taskManager.GetTaskStates();
|
||||
}
|
||||
}
|
||||
78
RobotNet.ScriptManager/Hubs/VariablesHub.cs
Normal file
78
RobotNet.ScriptManager/Hubs/VariablesHub.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
|
||||
namespace RobotNet.ScriptManager.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class VariablesHub(ScriptGlobalsManager globalsManager) : Hub
|
||||
{
|
||||
public string GetString(string name)
|
||||
{
|
||||
if (globalsManager.GetVariableType(name) == typeof(string) && globalsManager.Globals.TryGetValue(name, out var value) && value is string strValue)
|
||||
{
|
||||
return strValue;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public int GetInt(string name)
|
||||
{
|
||||
if (globalsManager.GetVariableType(name) == typeof(int) && globalsManager.Globals.TryGetValue(name, out var value) && value is int intValue)
|
||||
{
|
||||
return intValue;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool GetBool(string name)
|
||||
{
|
||||
if (globalsManager.GetVariableType(name) == typeof(bool) && globalsManager.Globals.TryGetValue(name, out var value) && value is bool boolValue)
|
||||
{
|
||||
return boolValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public double GetDouble(string name)
|
||||
{
|
||||
if (globalsManager.GetVariableType(name) == typeof(double) && globalsManager.Globals.TryGetValue(name, out var value) && value is double doubleValue)
|
||||
{
|
||||
return doubleValue;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public void SetString(string name, string value)
|
||||
{
|
||||
if (globalsManager.GetVariableType(name) == typeof(double) && globalsManager.Globals.ContainsKey(name))
|
||||
{
|
||||
globalsManager.Globals[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetInt(string name, int value)
|
||||
{
|
||||
if (globalsManager.GetVariableType(name) == typeof(int) && globalsManager.Globals.ContainsKey(name))
|
||||
{
|
||||
globalsManager.Globals[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBool(string name, bool value)
|
||||
{
|
||||
if (globalsManager.GetVariableType(name) == typeof(bool) && globalsManager.Globals.ContainsKey(name))
|
||||
{
|
||||
globalsManager.Globals[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDouble(string name, double value)
|
||||
{
|
||||
if (globalsManager.GetVariableType(name) == typeof(double) && globalsManager.Globals.ContainsKey(name))
|
||||
{
|
||||
globalsManager.Globals[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
RobotNet.ScriptManager/Models/ConsoleLog.cs
Normal file
25
RobotNet.ScriptManager/Models/ConsoleLog.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ConsoleLog(IHubContext<ConsoleHub> consoleHub, ILogger? logger = null) : Script.ILogger
|
||||
{
|
||||
public void LogError(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageError", message));
|
||||
logger?.LogError(message);
|
||||
}
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageInfo", message));
|
||||
logger?.LogInformation(message);
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageWarning", message));
|
||||
logger?.LogWarning(message);
|
||||
}
|
||||
}
|
||||
63
RobotNet.ScriptManager/Models/ScriptGlobalsRobotNet.cs
Normal file
63
RobotNet.ScriptManager/Models/ScriptGlobalsRobotNet.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public abstract class ScriptGlobalsRobotNet(Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory) : IRobotNetGlobals
|
||||
{
|
||||
public Script.ILogger Logger { get; } = logger;
|
||||
|
||||
public IRobotManager RobotManager { get; } = robotManager;
|
||||
|
||||
public IMapManager MapManager { get; } = mapManager;
|
||||
|
||||
public virtual Guid CurrentMissionId => throw new NotImplementedException();
|
||||
|
||||
public IUnixDevice UnixDevice { get; } = Connections.UnixDevice.Instance;
|
||||
|
||||
public IConnectionManager ConnectionManager { get; } = connectionManager;
|
||||
|
||||
public async Task<Guid> CreateMission(string name, params object[] parameters)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var missionCreator = scope.ServiceProvider.GetRequiredService<ScriptMissionCreator>();
|
||||
return await missionCreator.CreateMissionAsync(name, []);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateMission(string name) => await CreateMission(name, []);
|
||||
|
||||
public bool CancelMission(Guid missionId, string reason)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var missionManager = scope.ServiceProvider.GetRequiredService<ScriptMissionManager>();
|
||||
return missionManager.Cancel(missionId, reason);
|
||||
}
|
||||
|
||||
public bool DisableTask(string name)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var taskManager = scope.ServiceProvider.GetRequiredService<ScriptTaskManager>();
|
||||
return taskManager.Pause(name);
|
||||
}
|
||||
|
||||
public bool EnableTask(string name)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var taskManager = scope.ServiceProvider.GetRequiredService<ScriptTaskManager>();
|
||||
return taskManager.Resume(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ScriptGlobalsRobotNetTask(Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory)
|
||||
: ScriptGlobalsRobotNet(logger, robotManager, mapManager, connectionManager, scopeFactory)
|
||||
{
|
||||
public override Guid CurrentMissionId => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class ScriptGlobalsRobotNetMission(Guid id, Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory)
|
||||
: ScriptGlobalsRobotNet(logger, robotManager, mapManager, connectionManager, scopeFactory)
|
||||
{
|
||||
public override Guid CurrentMissionId { get; } = id;
|
||||
}
|
||||
109
RobotNet.ScriptManager/Models/ScriptMapElement.cs
Normal file
109
RobotNet.ScriptManager/Models/ScriptMapElement.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using RobotNet.MapShares;
|
||||
using RobotNet.MapShares.Dtos;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptMapElement(string mapName,
|
||||
ElementDto element,
|
||||
Func<string, string, bool, Task> UpdateIsOpenFunc,
|
||||
Func<string, string, ElementPropertyUpdateModel, Task>? UpdatePropertiesFunc) : Script.IElement
|
||||
{
|
||||
public Guid Id { get; } = element.Id;
|
||||
|
||||
public Guid ModelId { get; } = element.ModelId;
|
||||
|
||||
public string ModelName { get; } = element.ModelName ?? throw new ArgumentNullException(nameof(element), "Model name cannot be null");
|
||||
|
||||
public Guid NodeId { get; } = element.NodeId;
|
||||
|
||||
public string MapName { get; } = mapName ?? throw new ArgumentNullException(nameof(mapName), "Map name cannot be null");
|
||||
|
||||
public string Name { get; } = element.Name ?? throw new ArgumentNullException(nameof(element), "Element name cannot be null");
|
||||
|
||||
public double OffsetX { get; } = element.OffsetX;
|
||||
|
||||
public double OffsetY { get; } = element.OffsetY;
|
||||
|
||||
public string NodeName { get; } = element.NodeName ?? throw new ArgumentNullException(nameof(element), "Node name cannot be null");
|
||||
|
||||
public double X { get; } = element.X;
|
||||
|
||||
public double Y { get; } = element.Y;
|
||||
|
||||
public double Theta { get; } = element.Theta;
|
||||
|
||||
public Script.Expressions.ElementProperties Properties { get; } = MapManagerExtensions.GetElementProperties(element.IsOpen, element.Content);
|
||||
|
||||
private Script.Expressions.ElementProperties StoredProperties = MapManagerExtensions.GetElementProperties(element.IsOpen, element.Content);
|
||||
|
||||
public async Task SaveChangesAsync()
|
||||
{
|
||||
if (Properties.IsOpen != StoredProperties.IsOpen && UpdateIsOpenFunc != null)
|
||||
{
|
||||
await UpdateIsOpenFunc.Invoke(MapName, Name, Properties.IsOpen);
|
||||
StoredProperties.IsOpen = Properties.IsOpen;
|
||||
}
|
||||
|
||||
if (UpdatePropertiesFunc != null)
|
||||
{
|
||||
var changedProperties = new List<MapShares.Property.ElementProperty>();
|
||||
foreach (var prop in StoredProperties.Bool)
|
||||
{
|
||||
if (Properties.Bool.TryGetValue(prop.Key, out var value) && value != prop.Value)
|
||||
{
|
||||
changedProperties.Add(new MapShares.Property.ElementProperty()
|
||||
{
|
||||
Name = prop.Key,
|
||||
DefaultValue = value.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in StoredProperties.Double)
|
||||
{
|
||||
if (Properties.Double.TryGetValue(prop.Key, out var value) && value != prop.Value)
|
||||
{
|
||||
changedProperties.Add(new MapShares.Property.ElementProperty()
|
||||
{
|
||||
Name = prop.Key,
|
||||
DefaultValue = value.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in StoredProperties.Int)
|
||||
{
|
||||
if (Properties.Int.TryGetValue(prop.Key, out var value) && value != prop.Value)
|
||||
{
|
||||
changedProperties.Add(new MapShares.Property.ElementProperty()
|
||||
{
|
||||
Name = prop.Key,
|
||||
DefaultValue = value.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in StoredProperties.String)
|
||||
{
|
||||
if (Properties.String.TryGetValue(prop.Key, out var value) && value != prop.Value)
|
||||
{
|
||||
changedProperties.Add(new MapShares.Property.ElementProperty()
|
||||
{
|
||||
Name = prop.Key,
|
||||
DefaultValue = value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.Count != 0)
|
||||
{
|
||||
var updateModel = new ElementPropertyUpdateModel
|
||||
{
|
||||
Properties = [.. changedProperties]
|
||||
};
|
||||
await UpdatePropertiesFunc.Invoke(MapName, Name, updateModel);
|
||||
StoredProperties = Properties; // Update stored properties after saving
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
319
RobotNet.ScriptManager/Models/ScriptMapManager.cs
Normal file
319
RobotNet.ScriptManager/Models/ScriptMapManager.cs
Normal file
@@ -0,0 +1,319 @@
|
||||
using OpenIddict.Client;
|
||||
using RobotNet.MapShares.Dtos;
|
||||
using RobotNet.MapShares.Models;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Shares;
|
||||
using Serialize.Linq.Serializers;
|
||||
using System.Linq.Expressions;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptMapManager(OpenIddictClientService openIddictClient, string MapManagerUrl, string[] MapManagerScopes) : IMapManager
|
||||
{
|
||||
private string? CachedToken;
|
||||
private DateTime TokenExpiry;
|
||||
private static readonly ExpressionSerializer expressionSerializer = new(new Serialize.Linq.Serializers.JsonSerializer());
|
||||
|
||||
public Task<IElement[]> FindElements(string map, string model)
|
||||
=> FindElements(map, model, element => true);
|
||||
|
||||
public Task<IElement[]> FindElements(string map, string model, Expression<Func<Script.Expressions.ElementProperties, bool>> expr)
|
||||
=> FindElements(map, model, expr, true);
|
||||
|
||||
private async Task<IElement[]> FindElements(string map, string model, Expression<Func<Script.Expressions.ElementProperties, bool>> expr, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, $"api/ScriptElements");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
// Đưa modelExpression vào body của request
|
||||
request.Content = JsonContent.Create(new ElementExpressionModel
|
||||
{
|
||||
MapName = map,
|
||||
ModelName = model,
|
||||
Expression = expressionSerializer.SerializeText(expr),
|
||||
});
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
return await FindElements(map, model, expr, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get elements: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult<IEnumerable<ElementDto>>>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
else if (result.Data == null || !result.Data.Any())
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return [.. result.Data.Select(e => new ScriptMapElement(map, e, UpdateIsOpenAsync, UpdatePropertiesAsync))];
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IElement> GetElement(string map, string name) => GetElement(map, name, true);
|
||||
|
||||
private async Task<IElement> GetElement(string map, string name, bool retry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(map) || string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentException("Map name and element name cannot be null or empty");
|
||||
}
|
||||
|
||||
var accessToken = await RequestAccessToken();
|
||||
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"api/ScriptElements/{map}/element/{name}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
return await GetElement(map, name, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get element: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult<ElementDto>>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
else if (result.Data == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Element not found in response");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ScriptMapElement(map, result.Data, UpdateIsOpenAsync, UpdatePropertiesAsync);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task UpdateIsOpenAsync(string mapName, string elementName, bool isOpen) => UpdateIsOpenAsync(mapName, elementName, isOpen, true);
|
||||
|
||||
private async Task UpdateIsOpenAsync(string mapName, string elementName, bool isOpen, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Patch, $"api/ScriptElements/{mapName}/element/{elementName}/IsOpen?isOpen={isOpen}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
//request.Content = JsonContent.Create(new { isOpen });
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
await UpdateIsOpenAsync(mapName, elementName, isOpen, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to update IsOpen: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
private Task UpdatePropertiesAsync(string mapName, string elementName, ElementPropertyUpdateModel model)
|
||||
=> UpdatePropertiesAsync(mapName, elementName, model, true);
|
||||
|
||||
private async Task UpdatePropertiesAsync(string mapName, string elementName, ElementPropertyUpdateModel model, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Patch, $"api/ScriptElements/{mapName}/element/{elementName}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
// Đưa modelExpression vào body của request
|
||||
request.Content = JsonContent.Create(model);
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
await UpdatePropertiesAsync(mapName, elementName, model, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get elements: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> RequestAccessToken()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(CachedToken) && DateTime.UtcNow < TokenExpiry)
|
||||
{
|
||||
return CachedToken;
|
||||
}
|
||||
|
||||
var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new()
|
||||
{
|
||||
Scopes = [.. MapManagerScopes],
|
||||
});
|
||||
|
||||
if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
TokenExpiry = result.AccessTokenExpirationDate.Value.UtcDateTime;
|
||||
CachedToken = result.AccessToken;
|
||||
return CachedToken;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<INode> GetNode(string map, string name) => GetNode(map, name, true);
|
||||
|
||||
private async Task<INode> GetNode(string map, string name, bool retry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(map) || string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentException("Map name and element name cannot be null or empty");
|
||||
}
|
||||
|
||||
var accessToken = await RequestAccessToken();
|
||||
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"api/ScriptElements/{map}/node/{name}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
return await GetNode(map, name, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get element: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult<NodeDto>>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
else if (result.Data == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Element not found in response");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ScriptMapNode(result.Data.Id, result.Data.MapId, result.Data.Name, result.Data.X, result.Data.Y, result.Data.Theta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
RobotNet.ScriptManager/Models/ScriptMapNode.cs
Normal file
3
RobotNet.ScriptManager/Models/ScriptMapNode.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public record ScriptMapNode(Guid Id, Guid MapId, string Name, double X, double Y, double Theta) : Script.INode;
|
||||
298
RobotNet.ScriptManager/Models/ScriptMission.cs
Normal file
298
RobotNet.ScriptManager/Models/ScriptMission.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Shares;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptMissionGlobals(IDictionary<string, object?> _globals, IDictionary<string, object?> _robotnet, ConcurrentDictionary<string, object?> _parameters)
|
||||
{
|
||||
public readonly IDictionary<string, object?> globals = _globals;
|
||||
public readonly IDictionary<string, object?> robotnet = _robotnet;
|
||||
public readonly IDictionary<string, object?> parameters = _parameters;
|
||||
}
|
||||
public class ScriptMission : IDisposable
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public string Name { get; }
|
||||
public MissionStatus Status { get; private set; } = MissionStatus.Idle;
|
||||
public Exception? Exception { get; private set; } = null;
|
||||
public CancellationToken CancellationToken => internalCts?.Token ?? CancellationToken.None;
|
||||
public WaitHandle WaitHandle => waitHandle;
|
||||
|
||||
private readonly ScriptRunner<IAsyncEnumerable<MissionState>> Runner;
|
||||
private readonly ScriptMissionGlobals Globals;
|
||||
private readonly CancellationTokenSource internalCts;
|
||||
private readonly Thread thread;
|
||||
private bool disposed;
|
||||
private readonly Mutex mutex = new();
|
||||
private readonly ScriptMissionLogger? Logger;
|
||||
private readonly ManualResetEvent waitHandle = new(false);
|
||||
|
||||
public ScriptMission(Guid id, string name, ScriptRunner<IAsyncEnumerable<MissionState>> runner, ScriptMissionGlobals globals, CancellationTokenSource cts)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Runner = runner;
|
||||
Globals = globals;
|
||||
internalCts = cts;
|
||||
thread = new Thread(() => Run(internalCts.Token)) { IsBackground = true, Priority = ThreadPriority.Highest };
|
||||
|
||||
if (Globals.robotnet.TryGetValue($"get_{nameof(IRobotNetGlobals.Logger)}", out object? get_logger)
|
||||
&& get_logger is Func<RobotNet.Script.ILogger> get_logger_func)
|
||||
{
|
||||
Logger = get_logger_func.Invoke() as ScriptMissionLogger;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetLog() => Logger?.GetLog() ?? string.Empty;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (!mutex.WaitOne(1000)) return;
|
||||
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission));
|
||||
if (Status != MissionStatus.Idle) throw new InvalidOperationException("Mission can only be started after preparation.");
|
||||
if (internalCts.IsCancellationRequested) throw new InvalidOperationException("Mission is not prepared or has been canceled.");
|
||||
|
||||
Status = MissionStatus.Running;
|
||||
thread.Start();
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Pause()
|
||||
{
|
||||
if (!mutex.WaitOne(1000)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission));
|
||||
if (Status == MissionStatus.Running)
|
||||
{
|
||||
Status = MissionStatus.Pausing;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false; // Cannot pause if not running
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Resume()
|
||||
{
|
||||
if (!mutex.WaitOne(1000)) return false;
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission));
|
||||
if (Status == MissionStatus.Paused)
|
||||
{
|
||||
Status = MissionStatus.Resuming;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false; // Cannot resume if not paused
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Cancel(string reason)
|
||||
{
|
||||
if (!mutex.WaitOne(1000))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission));
|
||||
if (!internalCts.IsCancellationRequested)
|
||||
{
|
||||
internalCts.Cancel();
|
||||
}
|
||||
if (Status == MissionStatus.Canceling || Status == MissionStatus.Canceled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Status == MissionStatus.Idle)
|
||||
{
|
||||
Logger?.LogInfo($"{DateTime.UtcNow} Idle Mission '{Name}' cancel with reason: {reason}");
|
||||
Status = MissionStatus.Canceled;
|
||||
return true;
|
||||
}
|
||||
else if (Status == MissionStatus.Running || Status == MissionStatus.Pausing || Status == MissionStatus.Paused || Status == MissionStatus.Resuming)
|
||||
{
|
||||
Status = MissionStatus.Canceling;
|
||||
Logger?.LogInfo($"{DateTime.UtcNow} Mission '{Name}' canceling with reason: {reason}");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
private void Run(CancellationToken cancellationToken)
|
||||
{
|
||||
Task.Factory.StartNew(async Task () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
cancellationToken.Register(() =>
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
});
|
||||
await foreach (var state in await Runner.Invoke(Globals, cts.Token))
|
||||
{
|
||||
Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} with step: {state.Step}; message: {state.Message}");
|
||||
if (Status == MissionStatus.Pausing)
|
||||
{
|
||||
Status = MissionStatus.Paused;
|
||||
while (Status == MissionStatus.Paused && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (Status != MissionStatus.Canceling)
|
||||
{
|
||||
Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled externally.");
|
||||
Logger?.LogError(Exception);
|
||||
Status = MissionStatus.Error;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (Status == MissionStatus.Resuming)
|
||||
{
|
||||
Status = MissionStatus.Running;
|
||||
continue;
|
||||
}
|
||||
else if (Status == MissionStatus.Canceling)
|
||||
{
|
||||
if (internalCts == null || internalCts.IsCancellationRequested)
|
||||
{
|
||||
Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled externally.");
|
||||
Logger?.LogError(Exception);
|
||||
Status = MissionStatus.Error;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger?.LogError($"{DateTime.UtcNow} Mission {Name}-{Id} is canceling");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (Status == MissionStatus.Running)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (Status != MissionStatus.Error)
|
||||
{
|
||||
Status = MissionStatus.Error;
|
||||
Exception = new InvalidOperationException($"{DateTime.UtcNow} Mission {Name}-{Id} Unexpected status change: {Status}");
|
||||
Logger?.LogError(Exception);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Status == MissionStatus.Running)
|
||||
{
|
||||
Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} is completed");
|
||||
Status = MissionStatus.Completed;
|
||||
}
|
||||
else if (Status == MissionStatus.Canceling)
|
||||
{
|
||||
Logger?.LogError($"{DateTime.UtcNow} Mission {Name}-{Id} is canceled");
|
||||
Status = MissionStatus.Canceled;
|
||||
}
|
||||
else if (Status != MissionStatus.Error)
|
||||
{
|
||||
Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} Cancel from status {Status}");
|
||||
Logger?.LogError(Exception);
|
||||
Status = MissionStatus.Error;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException oce)
|
||||
{
|
||||
if (Status != MissionStatus.Canceling)
|
||||
{
|
||||
Status = MissionStatus.Error;
|
||||
Exception = oce;
|
||||
Logger?.LogError(oce.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled successfully.");
|
||||
Status = MissionStatus.Canceled;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Status = MissionStatus.Error;
|
||||
Exception = ex;
|
||||
Logger?.LogError(Exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
waitHandle.Set();
|
||||
Logger?.LogInfo($"{DateTime.UtcNow} End Mission wtih status: {Status}");
|
||||
}
|
||||
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Current).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
|
||||
if (!internalCts.IsCancellationRequested)
|
||||
{
|
||||
internalCts.Cancel();
|
||||
}
|
||||
thread.Join();
|
||||
internalCts.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
}
|
||||
4
RobotNet.ScriptManager/Models/ScriptMissionData.cs
Normal file
4
RobotNet.ScriptManager/Models/ScriptMissionData.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public record ScriptMissionParameter(string Name, Type Type, object? DefaultValue = null);
|
||||
public record ScriptMissionData(string Name, IEnumerable<ScriptMissionParameter> Parameters, string Code, string Script, bool IsMultipleRun);
|
||||
47
RobotNet.ScriptManager/Models/ScriptMissionLogger.cs
Normal file
47
RobotNet.ScriptManager/Models/ScriptMissionLogger.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptMissionLogger(IHubContext<ConsoleHub> consoleHub, Guid missionId) : Script.ILogger
|
||||
{
|
||||
private string log = "";
|
||||
private readonly Mutex mutexLog = new();
|
||||
public void LogError(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageError", message));
|
||||
|
||||
mutexLog.WaitOne();
|
||||
log += $"E {DateTime.UtcNow} {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
|
||||
public void LogError(Exception ex) => LogError($"{ex.GetType().FullName} {ex.Message}");
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageInfo", message));
|
||||
|
||||
mutexLog.WaitOne();
|
||||
log += $"I {DateTime.UtcNow} {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageWarning", message));
|
||||
|
||||
mutexLog.WaitOne();
|
||||
log += $"W {DateTime.UtcNow} {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
|
||||
public string GetLog()
|
||||
{
|
||||
mutexLog.WaitOne();
|
||||
var result = log;
|
||||
log = ""; // Clear log after reading
|
||||
mutexLog.ReleaseMutex();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
185
RobotNet.ScriptManager/Models/ScriptRobotManager.cs
Normal file
185
RobotNet.ScriptManager/Models/ScriptRobotManager.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using OpenIddict.Client;
|
||||
using RobotNet.MapShares.Models;
|
||||
using RobotNet.RobotShares.Models;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Expressions;
|
||||
using RobotNet.ScriptManager.Clients;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.Shares;
|
||||
using Serialize.Linq.Serializers;
|
||||
using System.Linq.Expressions;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptRobotManager(OpenIddictClientService openIddictClient, string RobotManagerUrl, string[] RobotManagerScopes) : IRobotManager
|
||||
{
|
||||
private string? CachedToken;
|
||||
private DateTime TokenExpiry;
|
||||
private static readonly ExpressionSerializer expressionSerializer = new(new Serialize.Linq.Serializers.JsonSerializer());
|
||||
|
||||
public async Task<IRobot?> GetRobotById(string robotId)
|
||||
{
|
||||
var robot = new RobotManagerHubClient(robotId, $"{RobotManagerUrl}/hubs/robot-manager", async () =>
|
||||
{
|
||||
var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new()
|
||||
{
|
||||
Scopes = [.. RobotManagerScopes],
|
||||
});
|
||||
if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return result.AccessToken;
|
||||
}
|
||||
});
|
||||
await robot.StartAsync();
|
||||
return robot;
|
||||
}
|
||||
|
||||
public Task<RobotState> GetRobotState(string robotId) => GetRobotState(robotId, true);
|
||||
public async Task<RobotState> GetRobotState(string robotId, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
throw new ArgumentException("Failed to get access token");
|
||||
}
|
||||
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(RobotManagerUrl)
|
||||
};
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"api/RobotManager/State/{robotId}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
// Retry with a new access token
|
||||
CachedToken = null; // Clear cached token to force a new request
|
||||
return await GetRobotState(robotId, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get robot state: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var state = await response.Content.ReadFromJsonAsync<MessageResult<RobotStateModel>>();
|
||||
if (state == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize robot state response");
|
||||
}
|
||||
else if (!state.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Robot Manager error: {state.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (state.Data == null)
|
||||
{
|
||||
throw new InvalidOperationException("Robot state data is null.");
|
||||
}
|
||||
return VDA5050ScriptHelper.ConvertToRobotState(state.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> SearchRobots(string map, string model)
|
||||
=> SearchRobots(map, model, robot => true, true);
|
||||
|
||||
public Task<IEnumerable<string>> SearchRobots(string map, string model, Expression<Func<RobotState, bool>> expr)
|
||||
=> SearchRobots(map, model, expr, true);
|
||||
|
||||
public async Task<IEnumerable<string>> SearchRobots(string map, string model, Expression<Func<RobotState, bool>> expr, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(RobotManagerUrl)
|
||||
};
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, $"api/RobotManager/Search");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
request.Content = JsonContent.Create(new ElementExpressionModel
|
||||
{
|
||||
MapName = map,
|
||||
ModelName = model,
|
||||
Expression = expressionSerializer.SerializeText(expr),
|
||||
});
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
// Retry with a new access token
|
||||
CachedToken = null; // Clear cached token to force a new request
|
||||
return await SearchRobots(map, model, expr, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired.");
|
||||
}
|
||||
}
|
||||
else if (response.EnsureSuccessStatusCode().StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to search robots: {response.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult<string[]>>() ?? throw new Exception("Failed to convert result from Robot Manager");
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result.Data ?? [];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Robot Manager error: {result.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> RequestAccessToken()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(CachedToken) && DateTime.UtcNow < TokenExpiry)
|
||||
{
|
||||
return CachedToken;
|
||||
}
|
||||
|
||||
var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new()
|
||||
{
|
||||
Scopes = [.. RobotManagerScopes],
|
||||
});
|
||||
|
||||
if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
TokenExpiry = result.AccessTokenExpirationDate.Value.UtcDateTime;
|
||||
CachedToken = result.AccessToken;
|
||||
return CachedToken;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
RobotNet.ScriptManager/Models/ScriptTask.cs
Normal file
155
RobotNet.ScriptManager/Models/ScriptTask.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using RobotNet.Script.Shares;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptTask : IDisposable
|
||||
{
|
||||
public bool Paused { get; private set; } = false;
|
||||
private readonly ScriptRunner<object> scriptRunner;
|
||||
private readonly ScriptTaskGlobals globals;
|
||||
private readonly int Interval;
|
||||
private readonly double ProcessTime;
|
||||
private CancellationTokenSource? internalCts;
|
||||
private Thread? thread;
|
||||
private bool disposed;
|
||||
private readonly string Name;
|
||||
private readonly RobotNet.Script.ILogger? Logger;
|
||||
private readonly bool AutoStart;
|
||||
|
||||
public class ScriptTaskGlobals(IDictionary<string, object?> _globals, IDictionary<string, object?> _robotnet)
|
||||
{
|
||||
public IDictionary<string, object?> globals = _globals;
|
||||
public IDictionary<string, object?> robotnet = _robotnet;
|
||||
}
|
||||
|
||||
public ScriptTask(ScriptTaskData task, ScriptOptions options, IDictionary<string, object?> _globals, IDictionary<string, object?> _robotnet)
|
||||
{
|
||||
var script = CSharpScript.Create(task.Script, options, globalsType: typeof(ScriptTaskGlobals));
|
||||
scriptRunner = script.CreateDelegate();
|
||||
|
||||
Name = task.Name;
|
||||
globals = new ScriptTaskGlobals(_globals, _robotnet);
|
||||
Interval = task.Interval;
|
||||
ProcessTime = Interval * 0.8;
|
||||
AutoStart = task.AutoStart;
|
||||
Paused = !task.AutoStart;
|
||||
|
||||
if (globals.robotnet.TryGetValue($"get_{nameof(IRobotNetGlobals.Logger)}", out object? get_logger)
|
||||
&& get_logger is Func<RobotNet.Script.ILogger> get_logger_func)
|
||||
{
|
||||
Logger = get_logger_func.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void Start(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Stop(); // Ensure previous thread is stopped before starting a new one
|
||||
|
||||
internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
Paused = !AutoStart;
|
||||
thread = new Thread(RunningHandler)
|
||||
{
|
||||
IsBackground = true,
|
||||
Priority = ThreadPriority.Highest,
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
Paused = true;
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
Paused = false;
|
||||
}
|
||||
|
||||
private void RunningHandler()
|
||||
{
|
||||
if (internalCts == null || internalCts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var cts = new CancellationTokenSource();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
int elapsed;
|
||||
int remaining;
|
||||
|
||||
internalCts.Token.Register(() =>
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromMilliseconds(Interval * 3));
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
while (!internalCts.IsCancellationRequested)
|
||||
{
|
||||
if (Paused)
|
||||
{
|
||||
Thread.Sleep(Interval); // Sleep briefly to avoid busy waiting
|
||||
continue;
|
||||
}
|
||||
stopwatch.Restart();
|
||||
scriptRunner.Invoke(globals, cts.Token).GetAwaiter().GetResult();
|
||||
|
||||
stopwatch.Stop();
|
||||
elapsed = (int)stopwatch.ElapsedMilliseconds;
|
||||
remaining = Interval - elapsed;
|
||||
|
||||
// If execution time exceeds ProcessTime, add another cycle
|
||||
if (elapsed > ProcessTime)
|
||||
{
|
||||
remaining += Interval;
|
||||
}
|
||||
|
||||
if (remaining > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.Sleep(remaining);
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
GC.Collect();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError($"Task \"{Name}\" execution failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (internalCts != null && !internalCts.IsCancellationRequested)
|
||||
{
|
||||
internalCts.Cancel();
|
||||
}
|
||||
|
||||
if (thread != null && thread.IsAlive)
|
||||
{
|
||||
//thread.Interrupt();
|
||||
thread.Join();
|
||||
}
|
||||
|
||||
internalCts?.Dispose();
|
||||
internalCts = null;
|
||||
thread = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed) return;
|
||||
Stop();
|
||||
disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
3
RobotNet.ScriptManager/Models/ScriptTaskData.cs
Normal file
3
RobotNet.ScriptManager/Models/ScriptTaskData.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public record ScriptTaskData(string Name, int Interval, bool AutoStart, string Code, string Script);
|
||||
22
RobotNet.ScriptManager/Models/ScriptTaskLogger.cs
Normal file
22
RobotNet.ScriptManager/Models/ScriptTaskLogger.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptTaskLogger(IHubContext<ConsoleHub> consoleHub, string name) : Script.ILogger
|
||||
{
|
||||
public void LogError(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageError", message));
|
||||
}
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageInfo", message));
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageWarning", message));
|
||||
}
|
||||
}
|
||||
23
RobotNet.ScriptManager/Models/ScriptVariable.cs
Normal file
23
RobotNet.ScriptManager/Models/ScriptVariable.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptVariableSyntax(string name, Type type, object? defaultValue, bool publicRead, bool publicWrite)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public Type Type { get; } = type;
|
||||
public string TypeName { get; } = CSharpSyntaxHelper.ToTypeString(type);
|
||||
public object? DefaultValue { get; } = defaultValue;
|
||||
public bool PublicRead { get; } = publicRead;
|
||||
public bool PublicWrite { get; } = publicWrite;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null) return false;
|
||||
if(obj is not ScriptVariableSyntax variable) return false;
|
||||
if (variable.Name != Name) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Name.GetHashCode();
|
||||
}
|
||||
139
RobotNet.ScriptManager/Program.cs
Normal file
139
RobotNet.ScriptManager/Program.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NLog.Web;
|
||||
using OpenIddict.Client;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
using RobotNet.OpenIddictClient;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
//builder.Logging.ClearProviders();
|
||||
builder.Host.UseNLog();
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||
|
||||
var openIddictOption = builder.Configuration.GetSection(nameof(OpenIddictClientProviderOptions)).Get<OpenIddictClientProviderOptions>()
|
||||
?? throw new InvalidOperationException("OpenID configuration not found or invalid format.");
|
||||
|
||||
builder.Services.AddDbContext<ScriptManagerDbContext>(options => options.UseSqlServer(connectionString));
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
builder.Services.AddOpenIddict()
|
||||
.AddValidation(options =>
|
||||
{
|
||||
// Note: the validation handler uses OpenID Connect discovery
|
||||
// to retrieve the address of the introspection endpoint.
|
||||
options.SetIssuer(openIddictOption.Issuer);
|
||||
options.AddAudiences(openIddictOption.Audiences);
|
||||
|
||||
// Configure the validation handler to use introspection and register the client
|
||||
// credentials used when communicating with the remote introspection endpoint.
|
||||
options.UseIntrospection()
|
||||
.SetClientId(openIddictOption.ClientId)
|
||||
.SetClientSecret(openIddictOption.ClientSecret);
|
||||
|
||||
// Register the System.Net.Http integration.
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
options.UseSystemNetHttp(httpOptions =>
|
||||
{
|
||||
httpOptions.ConfigureHttpClientHandler(context =>
|
||||
{
|
||||
context.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true;
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
options.UseSystemNetHttp();
|
||||
}
|
||||
|
||||
// Register the ASP.NET Core host.
|
||||
options.UseAspNetCore();
|
||||
})
|
||||
.AddClient(options =>
|
||||
{
|
||||
// Allow grant_type=client_credentials to be negotiated.
|
||||
options.AllowClientCredentialsFlow();
|
||||
|
||||
// Disable token storage, which is not necessary for non-interactive flows like
|
||||
// grant_type=password, grant_type=client_credentials or grant_type=refresh_token.
|
||||
options.DisableTokenStorage();
|
||||
|
||||
// Register the System.Net.Http integration and use the identity of the current
|
||||
// assembly as a more specific user agent, which can be useful when dealing with
|
||||
// providers that use the user agent as a way to throttle requests (e.g Reddit).
|
||||
options.UseSystemNetHttp()
|
||||
.SetProductInformation(typeof(Program).Assembly);
|
||||
|
||||
var registration = new OpenIddictClientRegistration
|
||||
{
|
||||
Issuer = new Uri(openIddictOption.Issuer, UriKind.Absolute),
|
||||
GrantTypes = { GrantTypes.ClientCredentials },
|
||||
ClientId = openIddictOption.ClientId,
|
||||
ClientSecret = openIddictOption.ClientSecret,
|
||||
};
|
||||
|
||||
foreach (var scope in openIddictOption.Scopes)
|
||||
{
|
||||
registration.Scopes.Add(scope);
|
||||
}
|
||||
|
||||
// Add a client registration matching the client application definition in the server project.
|
||||
options.AddRegistration(registration);
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<ScriptMissionCreator>();
|
||||
builder.Services.AddSingleton<ScriptStateManager>();
|
||||
builder.Services.AddSingleton<ScriptGlobalsManager>();
|
||||
builder.Services.AddSingleton<ScriptMissionManager>();
|
||||
builder.Services.AddSingleton<ScriptTaskManager>();
|
||||
builder.Services.AddSingleton<ScriptConnectionManager>();
|
||||
builder.Services.AddSingleton<DashboardPublisher>();
|
||||
builder.Services.AddSingleton<DashboardConfig>();
|
||||
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ScriptStateManager>());
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<DashboardPublisher>());
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<DashboardConfig>());
|
||||
|
||||
var app = builder.Build();
|
||||
await app.Services.SeedScriptManagerDbAsync();
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapControllers();
|
||||
|
||||
app.MapHub<RobotNet.ScriptManager.Hubs.ConsoleHub>("/hubs/console");
|
||||
app.MapHub<RobotNet.ScriptManager.Hubs.ProcessorHub>("/hubs/processor");
|
||||
app.MapHub<RobotNet.ScriptManager.Hubs.ScriptTaskHub>("/hubs/scripttask");
|
||||
app.MapHub<RobotNet.ScriptManager.Hubs.DashboardHub>("/hubs/dashboard");
|
||||
app.MapHub<RobotNet.ScriptManager.Hubs.ScriptOpenHub>("/hubs/script-open");
|
||||
app.MapHub<RobotNet.ScriptManager.Hubs.HMIHub>("/hubs/hmi");
|
||||
|
||||
app.Run();
|
||||
15
RobotNet.ScriptManager/Properties/launchSettings.json
Normal file
15
RobotNet.ScriptManager/Properties/launchSettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"workingDirectory": "$(TargetDir)",
|
||||
"applicationUrl": "https://localhost:7102",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
RobotNet.ScriptManager/RobotNet.ScriptManager.csproj
Normal file
29
RobotNet.ScriptManager/RobotNet.ScriptManager.csproj
Normal file
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="6.0.3" />
|
||||
<PackageReference Include="OpenIddict.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="OpenIddict.Validation.SystemNetHttp" Version="7.0.0" />
|
||||
<PackageReference Include="Serialize.Linq" Version="4.0.167" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RobotNet.Clients\RobotNet.Clients.csproj" />
|
||||
<ProjectReference Include="..\RobotNet.MapShares\RobotNet.MapShares.csproj" />
|
||||
<ProjectReference Include="..\RobotNet.OpenIddictClient\RobotNet.OpenIddictClient.csproj" />
|
||||
<ProjectReference Include="..\RobotNet.RobotShares\RobotNet.RobotShares.csproj" />
|
||||
<ProjectReference Include="..\RobotNet.Script.Shares\RobotNet.Script.Shares.csproj" />
|
||||
<ProjectReference Include="..\RobotNet.Script\RobotNet.Script.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
36
RobotNet.ScriptManager/Services/DashboardConfig.cs
Normal file
36
RobotNet.ScriptManager/Services/DashboardConfig.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class DashboardConfig : BackgroundService
|
||||
{
|
||||
public string[] MissionNames => [.. Config.MissionNames ?? []];
|
||||
private ConfigData Config;
|
||||
private const string DataPath = "dashboardConfig.json";
|
||||
private struct ConfigData
|
||||
{
|
||||
public List<string> MissionNames { get; set; }
|
||||
}
|
||||
|
||||
public async Task UpdateMissionNames(string[] names)
|
||||
{
|
||||
Config.MissionNames = [..names];
|
||||
await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config));
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (File.Exists(DataPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Config = JsonSerializer.Deserialize<ConfigData>(await File.ReadAllTextAsync(DataPath, CancellationToken.None));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
else await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
115
RobotNet.ScriptManager/Services/DashboardPublisher.cs
Normal file
115
RobotNet.ScriptManager/Services/DashboardPublisher.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares.Dashboard;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
using System;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class DashboardPublisher(DashboardConfig Config, IServiceProvider ServiceProvider, IConfiguration Configuration, IHubContext<DashboardHub> DashboardHub, ILogger<DashboardPublisher> Logger) : BackgroundService
|
||||
{
|
||||
private readonly int UpdateTime = Configuration.GetValue<int>("Dashboard:UpdateTimeMilliSeconds", 5000);
|
||||
private readonly int CycleDate = Configuration.GetValue<int>("Dashboard:CycleDate", 7);
|
||||
|
||||
private static TaktTimeMissionDto GetTaktTime(InstanceMission[] missions, DateTime date)
|
||||
{
|
||||
TaktTimeMissionDto taktTime = new() { Label = date.ToString("dd/MM/yyyy") };
|
||||
if (missions.Length > 0)
|
||||
{
|
||||
double TaktTimeAll = 0;
|
||||
foreach (var mission in missions)
|
||||
{
|
||||
var time = mission.StopedAt - mission.CreatedAt;
|
||||
if (time.TotalMinutes > 0)
|
||||
{
|
||||
TaktTimeAll += time.TotalMinutes;
|
||||
if (time.TotalMinutes < taktTime.Min || taktTime.Min == 0) taktTime.Min = time.TotalMinutes;
|
||||
if (time.TotalMinutes > taktTime.Max || taktTime.Max == 0) taktTime.Max = time.TotalMinutes;
|
||||
}
|
||||
}
|
||||
taktTime.Average = TaktTimeAll / missions.Length;
|
||||
}
|
||||
return taktTime;
|
||||
}
|
||||
|
||||
private bool IsMissionInConfig(string missionName)
|
||||
{
|
||||
return Config.MissionNames.Length == 0 || Config.MissionNames.Contains(missionName);
|
||||
}
|
||||
|
||||
public DashboardDto GetData()
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
var MissionDb = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();
|
||||
List<DailyPerformanceDto> TotalMissionPerformance = [];
|
||||
List<TaktTimeMissionDto> TaktTimeMissions = [];
|
||||
var startDate = DateTime.Today.Date.AddDays(-CycleDate);
|
||||
for (var i = startDate; i <= DateTime.Today.Date; i = i.AddDays(1))
|
||||
{
|
||||
var missions = MissionDb.InstanceMissions.Where(im => im.CreatedAt.Date == i.Date).ToList();
|
||||
missions = [.. missions.Where(im => IsMissionInConfig(im.MissionName))];
|
||||
var completedMissions = missions.Where(im => im.Status == Script.Shares.MissionStatus.Completed).ToList();
|
||||
var errorMissions = missions.Where(im => im.Status == Script.Shares.MissionStatus.Error).ToList();
|
||||
TotalMissionPerformance.Add(new DailyPerformanceDto
|
||||
{
|
||||
Label = i.ToString("dd/MM/yyyy"),
|
||||
Completed = completedMissions.Count,
|
||||
Error = errorMissions.Count,
|
||||
Other = missions.Count - completedMissions.Count - errorMissions.Count,
|
||||
});
|
||||
TaktTimeMissions.Add(GetTaktTime([.. completedMissions], i));
|
||||
}
|
||||
DailyPerformanceDto TodayPerformance = TotalMissionPerformance[^1];
|
||||
DailyPerformanceDto ThisWeekPerformance = new()
|
||||
{
|
||||
Completed = TotalMissionPerformance.Sum(dp => dp.Completed),
|
||||
Error = TotalMissionPerformance.Sum(dp => dp.Error),
|
||||
Other = TotalMissionPerformance.Sum(dp => dp.Other),
|
||||
};
|
||||
|
||||
var toDayMissions = MissionDb.InstanceMissions.Where(im => im.CreatedAt.Date == DateTime.Today.Date).ToList();
|
||||
toDayMissions = [.. toDayMissions.Where(im => IsMissionInConfig(im.MissionName))];
|
||||
var toDaycompletedMissions = toDayMissions.Where(im => im.Status == Script.Shares.MissionStatus.Completed).ToList();
|
||||
var toDayerrorMissions = toDayMissions.Where(im => im.Status == Script.Shares.MissionStatus.Error).ToList();
|
||||
var toDayTaktTime = GetTaktTime([.. toDaycompletedMissions], DateTime.Today);
|
||||
|
||||
DailyMissionDto DailyMission = new()
|
||||
{
|
||||
Completed = toDaycompletedMissions.Count,
|
||||
Error = toDayerrorMissions.Count,
|
||||
Total = toDayMissions.Count,
|
||||
CompletedRate = toDayMissions.Count > 0 ? (int)((toDaycompletedMissions.Count * 100.0 / toDayMissions.Count) + 0.5) : 0,
|
||||
TaktTimeMin = toDayTaktTime.Min,
|
||||
TaktTimeAverage = toDayTaktTime.Average,
|
||||
TaktTimeMax = toDayTaktTime.Max,
|
||||
RobotOnline = 1,
|
||||
};
|
||||
|
||||
|
||||
return new()
|
||||
{
|
||||
TaktTimeMissions = [.. TaktTimeMissions],
|
||||
ThisWeekPerformance = ThisWeekPerformance,
|
||||
TodayPerformance = TodayPerformance,
|
||||
TotalMissionPerformance = [.. TotalMissionPerformance],
|
||||
DailyMission = DailyMission,
|
||||
};
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(DashboardHub.Clients.All.SendAsync("DashboardDataUpdated", GetData(), cancellationToken: stoppingToken), Task.Delay(UpdateTime, stoppingToken));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning("Dashboard Publisher xảy ra lỗi: {ex}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
RobotNet.ScriptManager/Services/LoopService.cs
Normal file
45
RobotNet.ScriptManager/Services/LoopService.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public abstract class LoopService(int interval) : IHostedService
|
||||
{
|
||||
private readonly TimeSpan Interval = TimeSpan.FromMilliseconds(interval);
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||
private readonly Stopwatch stopwatch = new();
|
||||
private Timer? timer;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await BeforExecuteAsync(cancellationToken);
|
||||
stopwatch.Start();
|
||||
timer = new Timer(Callback, cancellationTokenSource, 0, Timeout.Infinite);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
timer?.Dispose();
|
||||
await AfterExecuteAsync(cancellationToken);
|
||||
}
|
||||
protected virtual Task BeforExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
protected abstract void Execute(CancellationToken stoppingToken);
|
||||
protected virtual Task AfterExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
private void Callback(object? state)
|
||||
{
|
||||
if (state is not CancellationTokenSource cts || cts.IsCancellationRequested) return;
|
||||
|
||||
Execute(cts.Token);
|
||||
|
||||
if (!cts.IsCancellationRequested)
|
||||
{
|
||||
long nextTrigger = Interval.Ticks - (stopwatch.ElapsedTicks % Interval.Ticks);
|
||||
if (nextTrigger <= 0) nextTrigger = Interval.Ticks;
|
||||
|
||||
int nextIntervalMs = (int)(nextTrigger / TimeSpan.TicksPerMillisecond);
|
||||
|
||||
timer?.Change(nextIntervalMs, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
RobotNet.ScriptManager/Services/ScriptConnectionManager.cs
Normal file
80
RobotNet.ScriptManager/Services/ScriptConnectionManager.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using RobotNet.Script;
|
||||
using RobotNet.ScriptManager.Connections;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptConnectionManager : IConnectionManager
|
||||
{
|
||||
private readonly SemaphoreSlim _connectLock = new(1, 1);
|
||||
private readonly Dictionary<string, IModbusTcpClient> ModbusTcpConnections = [];
|
||||
private readonly Dictionary<string, ICcLinkIeBasicClient> CcLinkConnections = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (var client in ModbusTcpConnections.Values)
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
ModbusTcpConnections.Clear();
|
||||
|
||||
foreach (var client in CcLinkConnections.Values)
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
CcLinkConnections.Clear();
|
||||
}
|
||||
|
||||
public async Task<IModbusTcpClient> ConnectModbusTcp(string ipAddress, int port, byte unitId)
|
||||
{
|
||||
string key = $"{ipAddress}:{port}:{unitId}";
|
||||
await _connectLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (ModbusTcpConnections.TryGetValue(key, out var existingClient))
|
||||
{
|
||||
return existingClient;
|
||||
}
|
||||
else
|
||||
{
|
||||
var client = new ModbusTcpClient(ipAddress, port, unitId);
|
||||
|
||||
await client.ConnectAsync();
|
||||
|
||||
ModbusTcpConnections.Add(key, client);
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICcLinkIeBasicClient> ConnectCcLink(IeBasicClientOptions option)
|
||||
{
|
||||
string key = $"{option.Host}:{option.Port}:{option.NetworkNo}:{option.ModuleIoNo}:{option.MultidropNo}";
|
||||
await _connectLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (CcLinkConnections.TryGetValue(key, out var existingClient))
|
||||
{
|
||||
return existingClient;
|
||||
}
|
||||
else
|
||||
{
|
||||
var client = new CcLinkIeBasicClient(option);
|
||||
await client.ConnectAsync();
|
||||
|
||||
CcLinkConnections.Add(key, client);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RobotNet.ScriptManager/Services/ScriptGlobalsManager.cs
Normal file
231
RobotNet.ScriptManager/Services/ScriptGlobalsManager.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using OpenIddict.Client;
|
||||
using RobotNet.OpenIddictClient;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
using RobotNet.ScriptManager.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptGlobalsManager
|
||||
{
|
||||
public IDictionary<string, object?> Globals => _globals;
|
||||
private readonly ConcurrentDictionary<string, object?> _globals = new();
|
||||
private readonly Dictionary<string, Type> _globalsTypes = [];
|
||||
private readonly Dictionary<string, ScriptVariableSyntax> variables = [];
|
||||
private readonly IHubContext<ConsoleHub> consoleHubContext;
|
||||
private readonly ScriptRobotManager robotManager;
|
||||
private readonly ScriptMapManager mapManager;
|
||||
private readonly ScriptConnectionManager ConnectionManager;
|
||||
private readonly IServiceScopeFactory scopeFactory;
|
||||
|
||||
public ScriptGlobalsManager(IConfiguration configuration, OpenIddictClientService openIddictClient, IHubContext<ConsoleHub> _consoleHubContext, ScriptConnectionManager connectionManager, IServiceScopeFactory _scopeFactory)
|
||||
{
|
||||
consoleHubContext = _consoleHubContext;
|
||||
this.scopeFactory = _scopeFactory;
|
||||
ConnectionManager = connectionManager;
|
||||
|
||||
var robotManagerOptions = configuration.GetSection("RobotManager").Get<OpenIddictResourceOptions>() ?? throw new InvalidOperationException("OpenID configuration not found or invalid format.");
|
||||
robotManager = new ScriptRobotManager(openIddictClient, robotManagerOptions.Url, robotManagerOptions.Scopes);
|
||||
|
||||
var mapManagerOptions = configuration.GetSection("MapManager").Get<OpenIddictResourceOptions>() ?? throw new InvalidOperationException("OpenID configuration not found or invalid format.");
|
||||
mapManager = new ScriptMapManager(openIddictClient, mapManagerOptions.Url, mapManagerOptions.Scopes);
|
||||
}
|
||||
|
||||
public void LoadVariables(IEnumerable<ScriptVariableSyntax> variableSynctaxs)
|
||||
{
|
||||
_globals.Clear();
|
||||
_globalsTypes.Clear();
|
||||
variables.Clear();
|
||||
foreach (var v in variableSynctaxs)
|
||||
{
|
||||
variables.Add(v.Name, new(v.Name, v.Type, v.DefaultValue, v.PublicRead, v.PublicWrite));
|
||||
_globals.TryAdd(v.Name, v.DefaultValue);
|
||||
_globalsTypes.TryAdd(v.Name, v.Type);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ScriptVariableDto> GetVariablesData()
|
||||
{
|
||||
var vars = new List<ScriptVariableDto>();
|
||||
foreach (var v in variables.Values)
|
||||
{
|
||||
if (v.PublicRead)
|
||||
{
|
||||
vars.Add(new ScriptVariableDto(v.Name, v.TypeName, _globals[v.Name]?.ToString() ?? "null"));
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
public Type? GetVariableType(string name)
|
||||
{
|
||||
if (_globalsTypes.TryGetValue(name, out var type))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> GetRobotNetTask(string name)
|
||||
{
|
||||
var globalRobotNet = new ScriptGlobalsRobotNetTask(new ScriptTaskLogger(consoleHubContext, name), robotManager, mapManager, ConnectionManager, scopeFactory);
|
||||
|
||||
return ConvertGlobalsToDictionary(globalRobotNet);
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> GetRobotNetMission(Guid missionId)
|
||||
{
|
||||
var globalRobotNet = new ScriptGlobalsRobotNetMission(missionId, new ScriptMissionLogger(consoleHubContext, missionId), robotManager, mapManager, ConnectionManager, scopeFactory);
|
||||
|
||||
return ConvertGlobalsToDictionary(globalRobotNet);
|
||||
}
|
||||
|
||||
public void SetValue(string name, string value)
|
||||
{
|
||||
if (variables.TryGetValue(name, out var variable))
|
||||
{
|
||||
if (variable.PublicWrite)
|
||||
{
|
||||
if (_globalsTypes.TryGetValue(name, out var type))
|
||||
{
|
||||
try
|
||||
{
|
||||
var convertedValue = Convert.ChangeType(value, type);
|
||||
_globals[name] = convertedValue;
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot convert value '{value}' to type '{type.FullName}' for variable '{name}'.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"Variable type for '{name}' not found.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Variable '{name}' is not writable.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"Variable '{name}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetValue(string name)
|
||||
{
|
||||
if (variables.TryGetValue(name, out var variable))
|
||||
{
|
||||
if (_globalsTypes.TryGetValue(name, out var type))
|
||||
{
|
||||
_globals[name] = Convert.ChangeType(variable.DefaultValue, type);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"Variable type for '{name}' not found.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"Variable '{name}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> ConvertGlobalsToDictionary(IRobotNetGlobals globals)
|
||||
{
|
||||
var robotnet = new Dictionary<string, object?>();
|
||||
var type = typeof(IRobotNetGlobals);
|
||||
|
||||
|
||||
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
robotnet.TryAdd(field.Name, field.GetValue(globals));
|
||||
}
|
||||
|
||||
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.GetIndexParameters().Length == 0)
|
||||
{
|
||||
// Forward getter if available
|
||||
if (prop.GetGetMethod() != null)
|
||||
{
|
||||
var getter = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType([prop.PropertyType]),
|
||||
globals,
|
||||
prop.GetGetMethod()!
|
||||
);
|
||||
robotnet.TryAdd($"get_{prop.Name}", getter);
|
||||
}
|
||||
// Forward setter if available
|
||||
if (prop.GetSetMethod() != null)
|
||||
{
|
||||
var setter = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType([prop.PropertyType, typeof(void)]),
|
||||
globals,
|
||||
prop.GetSetMethod()!
|
||||
);
|
||||
robotnet.TryAdd($"set_{prop.Name}", setter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle indexers (properties with parameters)
|
||||
var indexParams = prop.GetIndexParameters().Select(p => p.ParameterType).ToArray();
|
||||
|
||||
// Forward indexer getter if available
|
||||
if (prop.GetGetMethod() != null)
|
||||
{
|
||||
var getterParamTypes = indexParams.Concat([prop.PropertyType]).ToArray();
|
||||
var getterDelegate = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType(getterParamTypes),
|
||||
globals,
|
||||
prop.GetGetMethod()!
|
||||
);
|
||||
robotnet.TryAdd($"get_{prop.Name}_indexer", getterDelegate);
|
||||
}
|
||||
|
||||
// Forward indexer setter if available
|
||||
if (prop.GetSetMethod() != null)
|
||||
{
|
||||
var setterParamTypes = indexParams.Concat([prop.PropertyType, typeof(void)]).ToArray();
|
||||
var setterDelegate = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType(setterParamTypes),
|
||||
globals,
|
||||
prop.GetSetMethod()!
|
||||
);
|
||||
robotnet.TryAdd($"set_{prop.Name}_indexer", setterDelegate);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (!method.IsSpecialName)
|
||||
{
|
||||
var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.FullName} {p.Name}"));
|
||||
var args = string.Join(", ", method.GetParameters().Select(p => p.Name));
|
||||
var returnType = method.ReturnType == typeof(void) ? "void" : method.ReturnType.FullName;
|
||||
|
||||
var paramTypes = string.Join(",", method.GetParameters().Select(p => p.ParameterType.FullName));
|
||||
var methodKey = $"{method.Name}({paramTypes})";
|
||||
|
||||
var del = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType([.. method.GetParameters().Select(p => p.ParameterType), method.ReturnType]),
|
||||
globals,
|
||||
method
|
||||
);
|
||||
// TODO: tôi muốn thay vì sử dụng tên phương thức, tôi muốn sử dụng tên phương thức với danh sánh kiểu dữ liệu của các tham số trong dấu ngoặc tròn
|
||||
robotnet.TryAdd(methodKey, del);
|
||||
}
|
||||
}
|
||||
return robotnet;
|
||||
}
|
||||
}
|
||||
63
RobotNet.ScriptManager/Services/ScriptMissionCreator.cs
Normal file
63
RobotNet.ScriptManager/Services/ScriptMissionCreator.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptMissionCreator(ScriptMissionManager missionManager, ScriptManagerDbContext scriptManagerDb)
|
||||
{
|
||||
public async Task<Guid> CreateMissionAsync(string name, IDictionary<string, string> parameters)
|
||||
{
|
||||
if (!missionManager.ContainsMissionName(name))
|
||||
throw new Exception($"Mission {name} không tồn tại");
|
||||
|
||||
var entry = scriptManagerDb.InstanceMissions.Add(new InstanceMission
|
||||
{
|
||||
MissionName = name,
|
||||
Parameters = System.Text.Json.JsonSerializer.Serialize(parameters),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Status = MissionStatus.Idle,
|
||||
});
|
||||
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
|
||||
try
|
||||
{
|
||||
missionManager.Create(entry.Entity.Id, name, parameters);
|
||||
return entry.Entity.Id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scriptManagerDb.InstanceMissions.Remove(entry.Entity);
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
throw new Exception($"Failed to create mission: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateMissionAsync(string name, object[] parameters)
|
||||
{
|
||||
if (!missionManager.ContainsMissionName(name))
|
||||
throw new Exception($"Mission {name} không tồn tại");
|
||||
|
||||
var entry = scriptManagerDb.InstanceMissions.Add(new InstanceMission
|
||||
{
|
||||
MissionName = name,
|
||||
Parameters = System.Text.Json.JsonSerializer.Serialize(parameters),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Status = MissionStatus.Idle,
|
||||
});
|
||||
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
|
||||
try
|
||||
{
|
||||
missionManager.Create(entry.Entity.Id, name, parameters);
|
||||
return entry.Entity.Id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scriptManagerDb.InstanceMissions.Remove(entry.Entity);
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
throw new Exception($"Failed to create mission: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
410
RobotNet.ScriptManager/Services/ScriptMissionManager.cs
Normal file
410
RobotNet.ScriptManager/Services/ScriptMissionManager.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.ScriptManager.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptMissionManager(ScriptGlobalsManager globalsManager, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
public ProcessorState State { get; private set; } = ProcessorState.Idle;
|
||||
private readonly Dictionary<string, ScriptMissionData> MissionDatas = [];
|
||||
private readonly Dictionary<string, ScriptRunner<IAsyncEnumerable<MissionState>>> Runners = [];
|
||||
private readonly ConcurrentDictionary<Guid, ScriptMission> Missions = [];
|
||||
private readonly ConcurrentQueue<ScriptMission> idleMissions = [];
|
||||
private readonly ConcurrentQueue<ScriptMission> runningMissions = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (State != ProcessorState.Idle && State != ProcessorState.Ready)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot reset missions while the processor is running.");
|
||||
}
|
||||
MissionDatas.Clear();
|
||||
Runners.Clear();
|
||||
|
||||
foreach (var mission in Missions.Values)
|
||||
{
|
||||
mission.Dispose();
|
||||
}
|
||||
Missions.Clear();
|
||||
|
||||
foreach (var mission in idleMissions)
|
||||
{
|
||||
mission.Dispose();
|
||||
}
|
||||
idleMissions.Clear();
|
||||
|
||||
foreach (var mission in runningMissions)
|
||||
{
|
||||
mission.Dispose();
|
||||
}
|
||||
runningMissions.Clear();
|
||||
|
||||
GC.Collect();
|
||||
|
||||
State = ProcessorState.Idle;
|
||||
}
|
||||
|
||||
public void LoadMissions(IEnumerable<ScriptMissionData> missionDatas)
|
||||
{
|
||||
if (State != ProcessorState.Idle && State != ProcessorState.Ready)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot load missions while the processor is running.");
|
||||
}
|
||||
|
||||
MissionDatas.Clear();
|
||||
Runners.Clear();
|
||||
runningMissions.Clear();
|
||||
idleMissions.Clear();
|
||||
runningMissions.Clear();
|
||||
|
||||
foreach (var mission in missionDatas)
|
||||
{
|
||||
MissionDatas.Add(mission.Name, mission);
|
||||
var script = CSharpScript.Create<IAsyncEnumerable<MissionState>>(mission.Script, ScriptConfiguration.ScriptOptions, globalsType: typeof(ScriptMissionGlobals));
|
||||
Runners.Add(mission.Name, script.CreateDelegate());
|
||||
}
|
||||
State = ProcessorState.Ready;
|
||||
}
|
||||
|
||||
public IEnumerable<ScriptMissionDto> GetMissionDatas() =>
|
||||
[.. MissionDatas.Values.Select(m => new ScriptMissionDto(m.Name, m.Parameters.Select(p => new ScriptMissionParameterDto(p.Name, p.Type.FullName ?? p.Type.Name, p.DefaultValue?.ToString())), m.Code))];
|
||||
|
||||
public bool ContainsMissionName(string name) => Runners.ContainsKey(name);
|
||||
|
||||
public void Create(Guid id, string name, IDictionary<string, string> parameterStrings)
|
||||
{
|
||||
if (!MissionDatas.TryGetValue(name, out var missionData)) throw new ArgumentException($"Mission data for '{name}' not found.");
|
||||
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(internalCts.Token);
|
||||
var parameters = new ConcurrentDictionary<string, object?>();
|
||||
bool hasCancellationToken = false;
|
||||
|
||||
foreach (var param in missionData.Parameters)
|
||||
{
|
||||
if (param.Type == typeof(CancellationToken))
|
||||
{
|
||||
if (hasCancellationToken)
|
||||
{
|
||||
throw new ArgumentException($"Mission '{name}' already has a CancellationToken parameter defined.");
|
||||
}
|
||||
hasCancellationToken = true;
|
||||
parameters.TryAdd(param.Name, cts.Token); // Use the internal CancellationTokenSource for the mission
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parameterStrings.TryGetValue(param.Name, out string? valueStr)) throw new ArgumentException($"Parameter '{param.Name}' not found in provided parameters.");
|
||||
|
||||
if (CSharpSyntaxHelper.ResolveValueFromString(valueStr, param.Type, out var value) && value != null)
|
||||
{
|
||||
parameters.TryAdd(param.Name, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Invalid value for parameter '{param.Name}': {valueStr}");
|
||||
}
|
||||
}
|
||||
|
||||
Create(id, name, parameters, cts);
|
||||
}
|
||||
|
||||
public void Create(Guid id, string name, object[] parameters)
|
||||
{
|
||||
if (!MissionDatas.TryGetValue(name, out var missionData)) throw new ArgumentException($"Mission data for '{name}' not found.");
|
||||
if (parameters.Length != missionData.Parameters.Count())
|
||||
{
|
||||
var count = missionData.Parameters.Count(p => p.Type == typeof(CancellationToken));
|
||||
if (count == 1)
|
||||
{
|
||||
if (parameters.Length != missionData.Parameters.Count() - count)
|
||||
{
|
||||
throw new ArgumentException($"Mission '{name}' expects {missionData.Parameters.Count()} parameters, but received {parameters.Length} without CancellationToken.");
|
||||
}
|
||||
}
|
||||
else if (count != 0)
|
||||
{
|
||||
throw new ArgumentException($"Mission '{name}' just have one CancellationToken, but received {parameters.Length}.");
|
||||
}
|
||||
}
|
||||
|
||||
var inputParameters = new ConcurrentDictionary<string, object?>();
|
||||
bool hasCancellationToken = false;
|
||||
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(internalCts.Token);
|
||||
int index = 0;
|
||||
foreach (var param in missionData.Parameters)
|
||||
{
|
||||
if (param.Type == typeof(CancellationToken))
|
||||
{
|
||||
if (hasCancellationToken)
|
||||
{
|
||||
throw new ArgumentException($"Mission '{name}' already has a CancellationToken parameter defined.");
|
||||
}
|
||||
hasCancellationToken = true;
|
||||
inputParameters.TryAdd(param.Name, cts.Token); // Use the internal CancellationTokenSource for the mission
|
||||
continue;
|
||||
}
|
||||
inputParameters.TryAdd(param.Name, parameters[index]);
|
||||
index++;
|
||||
}
|
||||
Create(id, name, inputParameters, cts);
|
||||
}
|
||||
|
||||
public void Create(Guid id, string name, ConcurrentDictionary<string, object?> parameters, CancellationTokenSource cts)
|
||||
{
|
||||
if (!Runners.TryGetValue(name, out var runner)) throw new ArgumentException($"Mission '{name}' not found.");
|
||||
|
||||
var robotnet = globalsManager.GetRobotNetMission(id);
|
||||
var mission = new ScriptMission(id, name, runner, new ScriptMissionGlobals(globalsManager.Globals, robotnet, parameters), cts);
|
||||
Missions.TryAdd(id, mission);
|
||||
idleMissions.Enqueue(mission);
|
||||
}
|
||||
|
||||
private CancellationTokenSource internalCts = new();
|
||||
private Thread? thread;
|
||||
public void Start(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Stop(); // Ensure previous thread is stopped before starting a new one
|
||||
ResetMissionDb();
|
||||
internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
var token = internalCts.Token;
|
||||
|
||||
thread = new Thread(() =>
|
||||
{
|
||||
State = ProcessorState.Running;
|
||||
while (!token.IsCancellationRequested || !runningMissions.IsEmpty)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
MiningIdleMissionHandle();
|
||||
MiningRunningMissionHandle();
|
||||
|
||||
stopwatch.Stop();
|
||||
int elapsed = (int)stopwatch.ElapsedMilliseconds;
|
||||
int remaining = 1000 - elapsed;
|
||||
|
||||
// If execution time exceeds ProcessTime, add another cycle
|
||||
if (elapsed > 900)
|
||||
{
|
||||
remaining += 1000;
|
||||
}
|
||||
|
||||
if (remaining > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.Sleep(remaining);
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State = ProcessorState.Ready;
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Priority = ThreadPriority.Highest,
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
public bool Pause(Guid id)
|
||||
{
|
||||
if (Missions.TryGetValue(id, out var mission))
|
||||
{
|
||||
return mission.Pause();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Resume(Guid id)
|
||||
{
|
||||
if (Missions.TryGetValue(id, out var mission))
|
||||
{
|
||||
return mission.Resume();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Cancel(Guid id, string reason)
|
||||
{
|
||||
if (Missions.TryGetValue(id, out var mission))
|
||||
{
|
||||
return mission.Cancel(reason);
|
||||
}
|
||||
|
||||
return false; // Mission not found or not running
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!idleMissions.IsEmpty || !runningMissions.IsEmpty)
|
||||
{
|
||||
var listWaitHandles = new List<WaitHandle>();
|
||||
while (idleMissions.TryDequeue(out var mission))
|
||||
{
|
||||
mission.Cancel("Cancel by script mission manager is stoped");
|
||||
listWaitHandles.Add(mission.WaitHandle);
|
||||
}
|
||||
while (runningMissions.TryDequeue(out var mission))
|
||||
{
|
||||
mission.Cancel("Cancel by script mission manager is stoped");
|
||||
listWaitHandles.Add(mission.WaitHandle);
|
||||
}
|
||||
|
||||
WaitHandle.WaitAll([.. listWaitHandles]);
|
||||
}
|
||||
|
||||
if (!internalCts.IsCancellationRequested)
|
||||
{
|
||||
internalCts.Cancel();
|
||||
}
|
||||
|
||||
if (thread != null && thread.IsAlive)
|
||||
{
|
||||
thread.Interrupt();
|
||||
thread.Join();
|
||||
}
|
||||
|
||||
internalCts.Dispose();
|
||||
thread = null;
|
||||
}
|
||||
|
||||
private void RemoveMission(ScriptMission mission)
|
||||
{
|
||||
mission.Dispose();
|
||||
Missions.TryRemove(mission.Id, out _);
|
||||
}
|
||||
|
||||
public void ResetMissionDb()
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();
|
||||
|
||||
var missions = dbContext.InstanceMissions.Where(m => m.Status == MissionStatus.Running
|
||||
|| m.Status == MissionStatus.Pausing
|
||||
|| m.Status == MissionStatus.Paused
|
||||
|| m.Status == MissionStatus.Canceling
|
||||
|| m.Status == MissionStatus.Resuming).ToList();
|
||||
foreach (var mission in missions)
|
||||
{
|
||||
mission.Log += $"{Environment.NewLine}{DateTime.UtcNow}: Mission Manager start, but instance mission has state {mission.Status}";
|
||||
mission.Status = MissionStatus.Error;
|
||||
}
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
private void MiningIdleMissionHandle()
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();
|
||||
|
||||
int count = idleMissions.Count;
|
||||
bool hasChanges = false;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!idleMissions.TryDequeue(out var mission)) break;
|
||||
|
||||
var dbMission = dbContext.InstanceMissions.Find(mission.Id);
|
||||
if (dbMission == null)
|
||||
{
|
||||
RemoveMission(mission);
|
||||
continue; // Skip if mission not found in database
|
||||
}
|
||||
|
||||
if (mission.Status == MissionStatus.Idle)
|
||||
{
|
||||
mission.Start();
|
||||
runningMissions.Enqueue(mission);
|
||||
dbMission.Status = mission.Status;
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveMission(mission);
|
||||
if (mission.Status == MissionStatus.Canceled)
|
||||
{
|
||||
dbMission.Status = MissionStatus.Canceled;
|
||||
dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
|
||||
}
|
||||
else
|
||||
{
|
||||
dbMission.Status = MissionStatus.Error;
|
||||
dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}{Environment.NewLine}{DateTime.UtcNow}: Mission is not in idle state. [{mission.Status}]";
|
||||
}
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
}
|
||||
if (hasChanges)
|
||||
{
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private void MiningRunningMissionHandle()
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();
|
||||
|
||||
bool hasChanges = false;
|
||||
int count = runningMissions.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!runningMissions.TryDequeue(out var mission)) break;
|
||||
|
||||
var dbMission = dbContext.InstanceMissions.Find(mission.Id);
|
||||
if (dbMission == null)
|
||||
{
|
||||
RemoveMission(mission);
|
||||
continue; // Skip if mission not found in database
|
||||
}
|
||||
|
||||
switch (mission.Status)
|
||||
{
|
||||
case MissionStatus.Running:
|
||||
case MissionStatus.Paused:
|
||||
case MissionStatus.Canceling:
|
||||
case MissionStatus.Resuming:
|
||||
case MissionStatus.Pausing:
|
||||
if (dbMission.Status != mission.Status)
|
||||
{
|
||||
dbMission.Status = mission.Status;
|
||||
hasChanges = true;
|
||||
}
|
||||
runningMissions.Enqueue(mission);
|
||||
break;
|
||||
case MissionStatus.Completed:
|
||||
case MissionStatus.Canceled:
|
||||
case MissionStatus.Error:
|
||||
dbMission.Status = mission.Status;
|
||||
dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
|
||||
dbMission.StopedAt = DateTime.UtcNow;
|
||||
hasChanges = true;
|
||||
RemoveMission(mission);
|
||||
break; // Handle these statuses in their respective methods
|
||||
default:
|
||||
dbMission.Status = MissionStatus.Error;
|
||||
dbMission.Log += $"{Environment.NewLine} Wrong mission status on running: {mission.Status}";
|
||||
dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
|
||||
hasChanges = true;
|
||||
RemoveMission(mission);
|
||||
continue; // Skip unknown statuses
|
||||
}
|
||||
}
|
||||
if (hasChanges)
|
||||
{
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
298
RobotNet.ScriptManager/Services/ScriptStateManager.cs
Normal file
298
RobotNet.ScriptManager/Services/ScriptStateManager.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
using RobotNet.ScriptManager.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptStateManager(ScriptTaskManager taskManager,
|
||||
ScriptMissionManager missionManager,
|
||||
ScriptGlobalsManager globalsManager,
|
||||
ScriptConnectionManager connectionManager,
|
||||
IHubContext<ProcessorHub> processorHubContext,
|
||||
IHubContext<HMIHub> hmiHubContext,
|
||||
IHubContext<ConsoleHub> consoleHubContext,
|
||||
ILogger<ScriptStateManager> logger) : LoopService(500)
|
||||
{
|
||||
public string StateMesssage { get; private set; } = "";
|
||||
public ProcessorState State { get; private set; } = ProcessorState.Idle;
|
||||
public ProcessorRequest Request { get; private set; } = ProcessorRequest.None;
|
||||
private readonly ConsoleLog consoleLog = new(consoleHubContext, logger);
|
||||
|
||||
private readonly Mutex mutex = new();
|
||||
private CancellationTokenSource? runningCancellation;
|
||||
|
||||
public bool Build(ref string message)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (mutex.WaitOne(1000))
|
||||
{
|
||||
if (Request != ProcessorRequest.None)
|
||||
{
|
||||
message = $"Không thể thực hiện build vì Processor đang thực hiện {Request}";
|
||||
}
|
||||
else if (State == ProcessorState.Running)
|
||||
{
|
||||
message = "Không thể thực hiện build vì Processor đang Running}";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
SetRequest(ProcessorRequest.Build);
|
||||
}
|
||||
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Không thể thực hiện build vì request timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Run(ref string message)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (mutex.WaitOne(1000))
|
||||
{
|
||||
if (Request != ProcessorRequest.None)
|
||||
{
|
||||
message = $"Không thể thực hiện run vì Processor đang thực hiện {Request}";
|
||||
}
|
||||
else if (State != ProcessorState.Ready)
|
||||
{
|
||||
message = $"Không thể thực hiện run vì Processor đang ở trạng thái {State}, không phải Ready";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
SetRequest(ProcessorRequest.Run);
|
||||
}
|
||||
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Không thể thực hiện run vì request timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Stop(ref string message)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (mutex.WaitOne(1000))
|
||||
{
|
||||
if (Request != ProcessorRequest.None)
|
||||
{
|
||||
message = $"Không thể thực hiện stop vì Processor đang thực hiện {Request}";
|
||||
}
|
||||
else if (State != ProcessorState.Running)
|
||||
{
|
||||
message = $"Không thể thực hiện stop vì Processor đang ở trạng thái {State}, không phải Running";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
SetRequest(ProcessorRequest.Stop);
|
||||
}
|
||||
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Không thể thực hiện stop vì request timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Reset(ref string message)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (mutex.WaitOne(1000))
|
||||
{
|
||||
if (Request != ProcessorRequest.None)
|
||||
{
|
||||
message = $"Không thể thực hiện reset vì Processor đang thực hiện {Request}";
|
||||
}
|
||||
else if (State != ProcessorState.Ready && State != ProcessorState.Error && State != ProcessorState.BuildError && State != ProcessorState.Idle)
|
||||
{
|
||||
message = $"Không thể thực hiện reset vì Processor đang ở trạng thái {State}, không phải Ready hoặc Error hoặc BuildError";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
SetRequest(ProcessorRequest.Reset);
|
||||
}
|
||||
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Không thể thực hiện reset vì request timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetRequest(ProcessorRequest request)
|
||||
{
|
||||
if (Request == request) return;
|
||||
|
||||
Request = request;
|
||||
_ = Task.Factory.StartNew(async () =>
|
||||
{
|
||||
await processorHubContext.Clients.All.SendAsync("RequestChanged", Request);
|
||||
await hmiHubContext.Clients.All.SendAsync("RequestChanged", Request);
|
||||
});
|
||||
}
|
||||
|
||||
private void SetState(ProcessorState state)
|
||||
{
|
||||
if (State == state) return;
|
||||
|
||||
State = state;
|
||||
_ = Task.Factory.StartNew(async () =>
|
||||
{
|
||||
await processorHubContext.Clients.All.SendAsync("StateChanged", State);
|
||||
await hmiHubContext.Clients.All.SendAsync("StateChanged", State);
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task BeforExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
missionManager.ResetMissionDb();
|
||||
SetRequest(ProcessorRequest.Build);
|
||||
await base.BeforExecuteAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override void Execute(CancellationToken stoppingToken)
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case ProcessorState.Idle:
|
||||
case ProcessorState.BuildError:
|
||||
case ProcessorState.Error:
|
||||
if (Request == ProcessorRequest.Build)
|
||||
{
|
||||
SetState(ProcessorState.Building);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
BuildHandler();
|
||||
}
|
||||
else if (Request == ProcessorRequest.Reset)
|
||||
{
|
||||
missionManager.Reset();
|
||||
taskManager.Reset();
|
||||
connectionManager.Reset();
|
||||
SetState(ProcessorState.Idle);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
}
|
||||
break;
|
||||
case ProcessorState.Ready:
|
||||
if (Request == ProcessorRequest.Build)
|
||||
{
|
||||
SetState(ProcessorState.Building);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
BuildHandler();
|
||||
}
|
||||
else if (Request == ProcessorRequest.Run)
|
||||
{
|
||||
SetState(ProcessorState.Running);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
RunHandler();
|
||||
}
|
||||
else if (Request == ProcessorRequest.Reset)
|
||||
{
|
||||
missionManager.Reset();
|
||||
taskManager.Reset();
|
||||
connectionManager.Reset();
|
||||
SetState(ProcessorState.Idle);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
}
|
||||
break;
|
||||
case ProcessorState.Running:
|
||||
if (Request == ProcessorRequest.Stop)
|
||||
{
|
||||
connectionManager.Reset();
|
||||
SetState(ProcessorState.Stopping);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
StopHandler();
|
||||
}
|
||||
break;
|
||||
case ProcessorState.Building:
|
||||
case ProcessorState.Stopping:
|
||||
case ProcessorState.Starting:
|
||||
default:
|
||||
SetRequest(ProcessorRequest.None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildHandler()
|
||||
{
|
||||
_ = Task.Factory.StartNew(() =>
|
||||
{
|
||||
consoleLog.LogInfo($"Start build all scripts");
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
var code = ScriptConfiguration.GetScriptCode();
|
||||
|
||||
string error = string.Empty;
|
||||
try
|
||||
{
|
||||
if (CSharpSyntaxHelper.VerifyScript(code, out error, out var variables, out var tasks, out var missions))
|
||||
{
|
||||
globalsManager.LoadVariables(variables);
|
||||
taskManager.LoadTasks(tasks);
|
||||
missionManager.LoadMissions(missions);
|
||||
watch.Stop();
|
||||
consoleLog.LogInfo($"Build all scripts successfully in {watch.ElapsedMilliseconds} ms");
|
||||
SetState(ProcessorState.Ready);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.ToString();
|
||||
}
|
||||
|
||||
watch.Stop();
|
||||
SetState(ProcessorState.BuildError);
|
||||
consoleLog.LogError(error);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void RunHandler()
|
||||
{
|
||||
_ = Task.Factory.StartNew(() =>
|
||||
{
|
||||
runningCancellation = new();
|
||||
taskManager.StartAll(runningCancellation.Token);
|
||||
missionManager.Start(runningCancellation.Token);
|
||||
SetState(ProcessorState.Running);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void StopHandler()
|
||||
{
|
||||
_ = Task.Factory.StartNew(() =>
|
||||
{
|
||||
runningCancellation?.Cancel();
|
||||
taskManager.StopAll();
|
||||
missionManager.Stop();
|
||||
SetState(ProcessorState.Ready);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
91
RobotNet.ScriptManager/Services/ScriptTaskManager.cs
Normal file
91
RobotNet.ScriptManager/Services/ScriptTaskManager.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
using RobotNet.ScriptManager.Models;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptTaskManager(ScriptGlobalsManager globalsManager, IHubContext<ScriptTaskHub> scriptHub)
|
||||
{
|
||||
private readonly List<ScriptTaskData> ScriptTaskDatas = [];
|
||||
private readonly Dictionary<string, ScriptTask> ScriptTasks = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
ScriptTaskDatas.Clear();
|
||||
foreach (var task in ScriptTasks.Values)
|
||||
{
|
||||
task.Dispose();
|
||||
}
|
||||
ScriptTasks.Clear();
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
public void LoadTasks(IEnumerable<ScriptTaskData> tasks)
|
||||
{
|
||||
Reset();
|
||||
ScriptTaskDatas.AddRange(tasks);
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
ScriptTasks.Add(task.Name, new ScriptTask(task, ScriptConfiguration.ScriptOptions, globalsManager.Globals, globalsManager.GetRobotNetTask(task.Name)));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ScriptTaskDto> GetTaskDatas() => [.. ScriptTaskDatas.Select(t => new ScriptTaskDto(t.Name, t.Interval, t.Code))];
|
||||
|
||||
public void StartAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var task in ScriptTasks.Values)
|
||||
{
|
||||
task.Start(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAll()
|
||||
{
|
||||
foreach (var task in ScriptTasks.Values)
|
||||
{
|
||||
task.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, bool> GetTaskStates()
|
||||
{
|
||||
return ScriptTasks.ToDictionary(task => task.Key, task => !task.Value.Paused);
|
||||
}
|
||||
|
||||
public bool Pause(string name)
|
||||
{
|
||||
if (ScriptTasks.TryGetValue(name, out var task))
|
||||
{
|
||||
task.Pause();
|
||||
_ = Task.Factory.StartNew(async Task () =>
|
||||
{
|
||||
await scriptHub.Clients.All.SendAsync("TaskPaused", name);
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Resume(string name)
|
||||
{
|
||||
if(ScriptTasks.TryGetValue(name, out var task))
|
||||
{
|
||||
task.Resume();
|
||||
_ = Task.Factory.StartNew(async Task () =>
|
||||
{
|
||||
await scriptHub.Clients.All.SendAsync("TaskResumed", name);
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
RobotNet.ScriptManager/appsettings.json
Normal file
44
RobotNet.ScriptManager/appsettings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=172.20.235.170;Database=RobotNet.Scripts;User Id=sa;Password=robotics@2022;TrustServerCertificate=True;MultipleActiveResultSets=true"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"System.Net.Http.HttpClient": "Warning",
|
||||
"OpenIddict.Validation.OpenIddictValidationDispatcher": "Warning",
|
||||
"OpenIddict.Client.OpenIddictClientDispatcher": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"OpenIddictClientProviderOptions": {
|
||||
"Issuer": "https://localhost:7061/",
|
||||
"Audiences": [
|
||||
"robotnet-script-manager"
|
||||
],
|
||||
"ClientId": "robotnet-script-manager",
|
||||
"ClientSecret": "05594ECB-BBAE-4246-8EED-4F0841C3B475",
|
||||
"Scopes": [
|
||||
"robotnet-robot-api",
|
||||
"robotnet-map-api"
|
||||
]
|
||||
},
|
||||
"RobotManager": {
|
||||
"Url": "https://localhost:7179",
|
||||
"Scopes": [
|
||||
"robotnet-robot-api"
|
||||
]
|
||||
},
|
||||
"MapManager": {
|
||||
"Url": "https://localhost:7177",
|
||||
"Scopes": [
|
||||
"robotnet-map-api"
|
||||
]
|
||||
},
|
||||
"Dashboard": {
|
||||
"UpdateTimeMilliSeconds": 5000,
|
||||
"CycleDate": 7
|
||||
}
|
||||
}
|
||||
24
RobotNet.ScriptManager/nlog.config
Normal file
24
RobotNet.ScriptManager/nlog.config
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
autoReload="true">
|
||||
|
||||
<extensions>
|
||||
<add assembly="NLog.Web.AspNetCore"/>
|
||||
</extensions>
|
||||
|
||||
<targets>
|
||||
<target xsi:type="File" name="scriptManagerLogFile" fileName="${basedir}/scriptManagerlogs/${shortdate}.log" maxArchiveFiles="90" archiveEvery="Day" >
|
||||
<layout type='JsonLayout'>
|
||||
<attribute name='time' layout='${date:format=HH\:mm\:ss.ffff}' />
|
||||
<attribute name='level' layout='${level:upperCase=true}'/>
|
||||
<attribute name='logger' layout='${logger}' />
|
||||
<attribute name='message' layout='${message}' />
|
||||
<attribute name='exception' layout='${exception:format=tostring}' />
|
||||
</layout>
|
||||
</target>
|
||||
</targets>
|
||||
<rules>
|
||||
<logger name="RobotNet.ScriptManager.*" minlevel="Debug" writeto="scriptManagerLogFile" />
|
||||
</rules>
|
||||
</nlog>
|
||||
5
RobotNet.ScriptManager/readme
Normal file
5
RobotNet.ScriptManager/readme
Normal file
@@ -0,0 +1,5 @@
|
||||
Tạo thư mục "scripts" và "dlls" trong thư mục build (RobotNet.ScriptManager\bin\Debug\net9.0)
|
||||
Sao chép RobotNet.Script.dll vào "dlls"
|
||||
Sao chép C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.7\System.Linq.Expressions.dll vào thư mục "dlls"
|
||||
Sao chép C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.7\System.Private.CoreLib.dll vào thư mục "dlls"
|
||||
Sao chép C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.7\System.Runtime.dll vào thư mục "dlls"
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>RobotNet.Script.Expressions</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="T:RobotNet.Script.Expressions.ElementProperties">
|
||||
<summary>
|
||||
Quản lý các thuộc tính của một phần tử trong bản đồ.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.ElementProperties.IsOpen">
|
||||
<summary>
|
||||
Gets or sets a value indicating whether the resource is currently open.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.ElementProperties.Bool">
|
||||
<summary>
|
||||
Gets a dictionary that maps string keys to boolean values.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.ElementProperties.Double">
|
||||
<summary>
|
||||
Gets a dictionary that maps string keys to double values.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.ElementProperties.Int">
|
||||
<summary>
|
||||
Gets a dictionary that maps string keys to integer values.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.ElementProperties.String">
|
||||
<summary>
|
||||
Gets a dictionary that maps string keys to string values.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:RobotNet.Script.Expressions.ElementProperties.#ctor(System.Boolean,System.Collections.Generic.IDictionary{System.String,System.Boolean},System.Collections.Generic.IDictionary{System.String,System.Double},System.Collections.Generic.IDictionary{System.String,System.Int32},System.Collections.Generic.IDictionary{System.String,System.String})">
|
||||
<summary>
|
||||
|
||||
</summary>
|
||||
<param name="isOpen"></param>
|
||||
<param name="dbool"></param>
|
||||
<param name="ddouble"></param>
|
||||
<param name="dint"></param>
|
||||
<param name="dstring"></param>
|
||||
</member>
|
||||
<member name="T:RobotNet.Script.Expressions.RobotState">
|
||||
<summary>
|
||||
Trạng thái hiện tại của robot, bao gồm thông tin về vị trí, trạng thái hoạt động, pin, v.v.
|
||||
</summary>
|
||||
<param name="IsReady"></param>
|
||||
<param name="Voltage"></param>
|
||||
<param name="IsLoading"></param>
|
||||
<param name="IsCharging"></param>
|
||||
<param name="X"></param>
|
||||
<param name="Y"></param>
|
||||
<param name="Theta"></param>
|
||||
</member>
|
||||
<member name="M:RobotNet.Script.Expressions.RobotState.#ctor(System.Boolean,System.Double,System.Boolean,System.Boolean,System.Double,System.Double,System.Double)">
|
||||
<summary>
|
||||
Trạng thái hiện tại của robot, bao gồm thông tin về vị trí, trạng thái hoạt động, pin, v.v.
|
||||
</summary>
|
||||
<param name="IsReady"></param>
|
||||
<param name="Voltage"></param>
|
||||
<param name="IsLoading"></param>
|
||||
<param name="IsCharging"></param>
|
||||
<param name="X"></param>
|
||||
<param name="Y"></param>
|
||||
<param name="Theta"></param>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.RobotState.IsReady">
|
||||
<summary></summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.RobotState.Voltage">
|
||||
<summary></summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.RobotState.IsLoading">
|
||||
<summary></summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.RobotState.IsCharging">
|
||||
<summary></summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.RobotState.X">
|
||||
<summary></summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.RobotState.Y">
|
||||
<summary></summary>
|
||||
</member>
|
||||
<member name="P:RobotNet.Script.Expressions.RobotState.Theta">
|
||||
<summary></summary>
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||
BIN
RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.dll
Normal file
BIN
RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.dll
Normal file
Binary file not shown.
1075
RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.xml
Normal file
1075
RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.xml
Normal file
File diff suppressed because it is too large
Load Diff
BIN
RobotNet.ScriptManager/wwwroot/dlls/System.Collections.dll
Normal file
BIN
RobotNet.ScriptManager/wwwroot/dlls/System.Collections.dll
Normal file
Binary file not shown.
BIN
RobotNet.ScriptManager/wwwroot/dlls/System.Linq.Expressions.dll
Normal file
BIN
RobotNet.ScriptManager/wwwroot/dlls/System.Linq.Expressions.dll
Normal file
Binary file not shown.
BIN
RobotNet.ScriptManager/wwwroot/dlls/System.Runtime.dll
Normal file
BIN
RobotNet.ScriptManager/wwwroot/dlls/System.Runtime.dll
Normal file
Binary file not shown.
Reference in New Issue
Block a user