first commit -push

This commit is contained in:
dungtt
2025-10-15 15:15:53 +07:00
parent 674ae395be
commit a9577c5756
885 changed files with 74595 additions and 0 deletions

View 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;
}
}

View 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;
}
}

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

View 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();
}
}

View File

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

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

View File

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

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

View 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();
}

View File

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

View 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; }
}

View 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
}
}
}

View File

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

View 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
}
}
}

View File

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

View 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
}
}
}

View File

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

View File

@@ -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
}
}
}

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

View 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();
}
}

View 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"]

View 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),
];
}

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

View 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
}) ?? []],
})
)
};
}
}

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

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

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

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

View 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;
}
}
}

View 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();
}
}

View 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;
}
}
}

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

View 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;
}

View 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
}
}
}
}

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

View 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;

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

View 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);

View 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;
}
}

View 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;
}
}
}

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

View File

@@ -0,0 +1,3 @@
namespace RobotNet.ScriptManager.Models;
public record ScriptTaskData(string Name, int Interval, bool AutoStart, string Code, string Script);

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

View 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();
}

View 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();

View 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"
}
}
}
}

View 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>

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

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

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

View 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();
}
}
}

View 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;
}
}

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

View 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();
}
}
}

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

View 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;
}
}
}

View 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
}
}

View 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>

View 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"

View File

@@ -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>

File diff suppressed because it is too large Load Diff

Binary file not shown.