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