Initial commit
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Antiforgery;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using RobotNet10.FleetManager.Components.Account.Pages;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal static class IdentityComponentsEndpointRouteBuilderExtensions
|
||||
{
|
||||
// These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project.
|
||||
public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
|
||||
var accountGroup = endpoints.MapGroup("/Account");
|
||||
|
||||
accountGroup.MapPost("/Logout", async (
|
||||
ClaimsPrincipal user,
|
||||
[FromServices] SignInManager<ApplicationUser> signInManager,
|
||||
[FromForm] string returnUrl) =>
|
||||
{
|
||||
await signInManager.SignOutAsync();
|
||||
return TypedResults.LocalRedirect($"~/{returnUrl}");
|
||||
});
|
||||
|
||||
return accountGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
|
||||
namespace RobotNet10.FleetManager.Components.Account
|
||||
{
|
||||
// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
|
||||
internal sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser>
|
||||
{
|
||||
private readonly IEmailSender emailSender = new NoOpEmailSender();
|
||||
|
||||
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
|
||||
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
|
||||
|
||||
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
|
||||
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
|
||||
|
||||
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
|
||||
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
|
||||
namespace RobotNet10.FleetManager.Components.Account
|
||||
{
|
||||
internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
|
||||
{
|
||||
public const string StatusCookieName = "Identity.StatusMessage";
|
||||
|
||||
private static readonly CookieBuilder StatusCookieBuilder = new()
|
||||
{
|
||||
SameSite = SameSiteMode.Strict,
|
||||
HttpOnly = true,
|
||||
IsEssential = true,
|
||||
MaxAge = TimeSpan.FromSeconds(5),
|
||||
};
|
||||
|
||||
public void RedirectTo(string? uri)
|
||||
{
|
||||
uri ??= "";
|
||||
|
||||
// Prevent open redirects.
|
||||
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
|
||||
{
|
||||
uri = navigationManager.ToBaseRelativePath(uri);
|
||||
}
|
||||
|
||||
navigationManager.NavigateTo(uri);
|
||||
}
|
||||
|
||||
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters)
|
||||
{
|
||||
var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path);
|
||||
var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters);
|
||||
RedirectTo(newUri);
|
||||
}
|
||||
|
||||
public void RedirectToWithStatus(string uri, string message, HttpContext context)
|
||||
{
|
||||
context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context));
|
||||
RedirectTo(uri);
|
||||
}
|
||||
|
||||
private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);
|
||||
|
||||
public void RedirectToCurrentPage() => RedirectTo(CurrentPath);
|
||||
|
||||
public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
|
||||
=> RedirectToWithStatus(CurrentPath, message, context);
|
||||
|
||||
public void RedirectToInvalidUser(UserManager<ApplicationUser> userManager, HttpContext context)
|
||||
=> RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace RobotNet10.FleetManager.Components.Account
|
||||
{
|
||||
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
|
||||
// every 30 minutes an interactive circuit is connected.
|
||||
internal sealed class IdentityRevalidatingAuthenticationStateProvider(
|
||||
ILoggerFactory loggerFactory,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<IdentityOptions> options)
|
||||
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
|
||||
{
|
||||
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
|
||||
|
||||
protected override async Task<bool> ValidateAuthenticationStateAsync(
|
||||
AuthenticationState authenticationState, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the user manager from a new scope to ensure it fetches fresh data
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal)
|
||||
{
|
||||
var user = await userManager.GetUserAsync(principal);
|
||||
if (user is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (!userManager.SupportsUserSecurityStamp)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
|
||||
var userStamp = await userManager.GetSecurityStampAsync(user);
|
||||
return principalStamp == userStamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
@page "/Account/Login"
|
||||
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using RobotNet10.FleetManager.Data
|
||||
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject RobotNet10.FleetManager.Services.Logger<Login> Logger
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<PageTitle>Log in</PageTitle>
|
||||
|
||||
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
|
||||
<h1>Log in</h1>
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
var statusMessageClass = errorMessage.StartsWith("Error") ? "danger" : "success";
|
||||
<div class="alert alert-@statusMessageClass" role="alert">
|
||||
@errorMessage
|
||||
</div>
|
||||
}
|
||||
<EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login" style="width: 300px;">
|
||||
<DataAnnotationsValidator />
|
||||
<hr />
|
||||
<ValidationSummary class="text-danger" role="alert" />
|
||||
<div class="form-floating mb-3">
|
||||
<InputText @bind-Value="Input.Username" class="form-control" autocomplete="username" aria-required="true" />
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<ValidationMessage For="() => Input.Username" class="text-danger" />
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<ValidationMessage For="() => Input.Password" class="text-danger" />
|
||||
</div>
|
||||
<div class="checkbox mb-3">
|
||||
<label class="form-label">
|
||||
<InputCheckbox @bind-Value="Input.RememberMe" class="darker-border-checkbox form-check-input" />
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
private string? errorMessage;
|
||||
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm]
|
||||
private InputModel Input { get; set; } = null!;
|
||||
|
||||
[SupplyParameterFromQuery]
|
||||
private string? ReturnUrl { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Input ??= new();
|
||||
base.OnInitialized();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (HttpMethods.IsGet(HttpContext.Request.Method))
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
}
|
||||
|
||||
errorMessage = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName];
|
||||
|
||||
if (errorMessage is not null)
|
||||
{
|
||||
HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoginUser()
|
||||
{
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await SignInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberMe, lockoutOnFailure: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Logger.Info("User logged in.");
|
||||
RedirectManager.RedirectTo(ReturnUrl);
|
||||
}
|
||||
else if (result.RequiresTwoFactor)
|
||||
{
|
||||
RedirectManager.RedirectTo(
|
||||
"Account/LoginWith2fa",
|
||||
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
Logger.Warning("User account locked out.");
|
||||
RedirectManager.RedirectTo("Account/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Error: Invalid login attempt.";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required]
|
||||
public string Username { get; set; } = "";
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
[Display(Name = "Remember me?")]
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@attribute [ExcludeFromInteractiveRouting]
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RobotNet10.FleetManager.Components.Account
|
||||
{
|
||||
public class PasskeyInputModel
|
||||
{
|
||||
public string? CredentialJson { get; set; }
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RobotNet10.FleetManager.Components.Account
|
||||
{
|
||||
public enum PasskeyOperation
|
||||
{
|
||||
Create = 0,
|
||||
Request = 1,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<ResourcePreloader />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["_content/RobotNet10.Components/styles.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["RobotNet10.FleetManager.styles.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["_content/RobotNet10.ScriptEditor/styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/svg+xml" href="_content/RobotNet10.Components/images/favicon.svg" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
|
||||
<script src="@Assets["_content/BlazorMonaco/jsInterop.js"]"></script>
|
||||
<script src="@Assets["_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"]"></script>
|
||||
<script src="@Assets["_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"]"></script>
|
||||
<script src="@Assets["_content/RobotNet10.Components/scripts.js"]"></script>
|
||||
<script src="@Assets["_content/RobotNet10.CustomConfigurationEditor/js/downloadFile.js"]"></script>
|
||||
<script src="@Assets["_content/RobotNet10.ScriptEditor/scripts.js"]"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Rejoining the server...
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Failed to rejoin.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Retry
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
The session has been paused by the server.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible">
|
||||
Resume
|
||||
</button>
|
||||
<p class="components-resume-failed-visible">
|
||||
Failed to resume the session.<br />Please reload the page.
|
||||
</p>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -0,0 +1,157 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: white;
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: #6b9ed2;
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
@page "/"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
<h1>Hello</h1>
|
||||
|
||||
|
||||
<AuthorizeView>
|
||||
<NotAuthorized>
|
||||
Vui lòng đăng nhập
|
||||
</NotAuthorized>
|
||||
<Authorized>
|
||||
Hello @context.User.Identity?.Name!
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@page "/not-found"
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
@@ -0,0 +1,12 @@
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(RobotNet10.Components.Layout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin />
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
@@ -0,0 +1,14 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using RobotNet10.FleetManager
|
||||
@using RobotNet10.FleetManager.Client
|
||||
@using RobotNet10.FleetManager.Components
|
||||
@using RobotNet10.FleetManager.Components.Layout
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace RobotNet10.FleetManager.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class LogsManagerController(Services.Logger<LogsManagerController> Logger) : ControllerBase
|
||||
{
|
||||
private readonly string LoggerDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
|
||||
|
||||
[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.Warning($"GetLogs: Invalid path detected.");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!System.IO.File.Exists(path))
|
||||
{
|
||||
Logger.Warning($"GetLogs: Log file not found for date {date:d} - {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.Warning($"GetLogs: System error occurred - {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Robot;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Responses;
|
||||
|
||||
namespace RobotNet10.FleetManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// API controller for managing robots.
|
||||
/// Provides RESTful endpoints for CRUD operations on robots.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All endpoints return appropriate HTTP status codes and error responses.
|
||||
/// Supports filtering by model ID and map ID.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("api/robots")]
|
||||
public class RobotController(IRobotService robotService, Services.Logger<RobotController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IRobotService _robotService = robotService;
|
||||
private readonly Services.Logger<RobotController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Get all robots with optional filters
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<RobotDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<RobotDto>>> GetAll([FromQuery] Guid? modelId, [FromQuery] Guid? mapId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robots = await _robotService.GetAllAsync(modelId, mapId);
|
||||
var dtos = robots.Select(r => MapToDto(r)).ToList();
|
||||
return Ok(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting all robots: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while retrieving robots." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(RobotDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RobotDto>> GetById(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robot = await _robotService.GetByIdAsync(id);
|
||||
if (robot == null)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Robot with ID {id} not found." });
|
||||
}
|
||||
|
||||
return Ok(MapToDto(robot));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting robot {id}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while retrieving the robot." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot by RobotId (string identifier)
|
||||
/// </summary>
|
||||
[HttpGet("robotId/{robotId}")]
|
||||
[ProducesResponseType(typeof(RobotDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RobotDto>> GetByRobotId(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robot = await _robotService.GetByRobotIdAsync(robotId);
|
||||
if (robot == null)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Robot with RobotId '{robotId}' not found." });
|
||||
}
|
||||
|
||||
return Ok(MapToDto(robot));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting robot with RobotId '{robotId}': {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while retrieving the robot." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search robots by query string
|
||||
/// </summary>
|
||||
[HttpGet("search")]
|
||||
[ProducesResponseType(typeof(List<RobotDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<RobotDto>>> Search([FromQuery] string query)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robots = await _robotService.SearchAsync(query);
|
||||
var dtos = robots.Select(r => MapToDto(r)).ToList();
|
||||
return Ok(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error searching robots with query '{query}': {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while searching robots." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all robots by model ID
|
||||
/// </summary>
|
||||
[HttpGet("model/{modelId}")]
|
||||
[ProducesResponseType(typeof(List<RobotDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<RobotDto>>> GetByModelId(Guid modelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robots = await _robotService.GetByModelIdAsync(modelId);
|
||||
var dtos = robots.Select(r => MapToDto(r)).ToList();
|
||||
return Ok(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting robots by model {modelId}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while retrieving robots." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new robot
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(RobotDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<RobotDto>> Create([FromBody] CreateRobotRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto
|
||||
{
|
||||
Error = "Validation failed",
|
||||
Details = ModelState.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => (object)(kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() ?? Array.Empty<string>()))
|
||||
});
|
||||
}
|
||||
|
||||
var robot = await _robotService.CreateAsync(request);
|
||||
var dto = MapToDto(robot);
|
||||
return CreatedAtAction(nameof(GetById), new { id = robot.Id }, dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error creating robot: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while creating the robot." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing robot
|
||||
/// </summary>
|
||||
[HttpPut("{id}")]
|
||||
[ProducesResponseType(typeof(RobotDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<RobotDto>> Update(Guid id, [FromBody] UpdateRobotRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto
|
||||
{
|
||||
Error = "Validation failed",
|
||||
Details = ModelState.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => (object)(kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() ?? Array.Empty<string>()))
|
||||
});
|
||||
}
|
||||
|
||||
var robot = await _robotService.UpdateAsync(id, request);
|
||||
var dto = MapToDto(robot);
|
||||
return Ok(dto);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error updating robot {id}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while updating the robot." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a robot
|
||||
/// </summary>
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _robotService.DeleteAsync(id);
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Robot with ID {id} not found." });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error deleting robot {id}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while deleting the robot." });
|
||||
}
|
||||
}
|
||||
|
||||
private static RobotDto MapToDto(Robot robot)
|
||||
{
|
||||
return new RobotDto
|
||||
{
|
||||
Id = robot.Id,
|
||||
RobotId = robot.RobotId,
|
||||
Name = robot.Name,
|
||||
ModelId = robot.ModelId,
|
||||
ModelName = robot.Model?.ModelName,
|
||||
MapId = robot.MapId,
|
||||
CreatedDate = robot.CreatedDate,
|
||||
UpdatedDate = robot.UpdatedDate
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.FleetManager.Services.RobotManager;
|
||||
using RobotNet10.FleetManager.Services.RobotManager.Models;
|
||||
using RobotNet10.FleetManager.Shared.Models;
|
||||
using RobotNet10.MapManager.Services;
|
||||
using RobotNet10.Shared;
|
||||
|
||||
namespace RobotNet10.FleetManager.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
public class RobotManagerController(IRobotService RobotService, ILayoutDataService LayoutManager, IRobotManagerService RobotManager, Services.Logger<RobotManagerController> Logger) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[Route("MoveToNode")]
|
||||
public async Task<MessageResult> MoveToNode([FromBody] RobotMoveToNodeModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(model.NodeName)) return new(false, "NodeName cannot be empty..");
|
||||
|
||||
var robot = await RobotService.GetByRobotIdAsync(model.RobotId);
|
||||
if (robot is null) return new(false, "RobotId does not exist.");
|
||||
if (robot.MapId is null || robot.MapId == Guid.Empty) return new(false, "The robot has not been assigned a map.");
|
||||
|
||||
var robotController = RobotManager.GetRobotController(model.RobotId);
|
||||
if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online.");
|
||||
if (robotController.Data is null || robotController.Data.State is null) return new(false, "The robot is broken connection.");
|
||||
|
||||
var map = await LayoutManager.GetLayoutDataAsync(robot.MapId ?? Guid.Empty);
|
||||
|
||||
var node = map.Nodes.FirstOrDefault(n => n.NodeName == model.NodeName && n.LevelId == map.LayoutLevelId);
|
||||
if (node is null) return new(false, "This Node does not exist.");
|
||||
|
||||
if (!robotController.IsReady) return new(false, "The robot is busy.");
|
||||
|
||||
var move = await robotController.MoveToNodeAsync(model.NodeName, model.LastAngle);
|
||||
if (move.IsSuccess) return new(true);
|
||||
Logger.Warning($"RobotManager API: MoveToNode for robot {model.RobotId} to node {model.NodeName} failed: {move.Message}");
|
||||
return new(false, "Request failed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"RobotManager API: MoveToNode for robot {model.RobotId} to node {model.NodeName} error: {ex.Message}");
|
||||
return new(false, "An error occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("MoveToNode/{robotId}")]
|
||||
public async Task<MessageResult> Cancel(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robot = await RobotService.GetByRobotIdAsync(robotId);
|
||||
if (robot is null) return new(false, "RobotId does not exist.");
|
||||
|
||||
var robotController = RobotManager.GetRobotController(robotId);
|
||||
if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online.");
|
||||
if (robotController.Data is null || robotController.Data.State is null) return new(false, "The robot is broken connection.");
|
||||
|
||||
var cancel = await robotController.CancelOrderAsync();
|
||||
if (cancel.IsSuccess) return new(true);
|
||||
Logger.Warning($"RobotManager API: Cancel order for robot {robotId} failed: {cancel.Message}");
|
||||
return new(false, "Request failed.");
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"RobotManager API: Cancel order for robot {robotId} error: {ex.Message}");
|
||||
return new(false, "An error occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("InstantActions")]
|
||||
public async Task<MessageResult> InstantAction([FromBody] RobotInstantActionModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robot = await RobotService.GetByRobotIdAsync(model.RobotId);
|
||||
if (robot is null) return new(false, "RobotId does not exist.");
|
||||
if (robot.MapId is null || robot.MapId == Guid.Empty) return new(false, "The robot has not been assigned a map.");
|
||||
|
||||
var robotController = RobotManager.GetRobotController(model.RobotId);
|
||||
if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online.");
|
||||
if (robotController.Data is null || robotController.Data.State is null) return new(false, "The robot is broken connection.");
|
||||
|
||||
var instantAction = await robotController.SendInstantActionAsync(model.Action);
|
||||
if (instantAction.IsSuccess) return new(true);
|
||||
Logger.Warning($"RobotManager API: Send instant action for robot {model.RobotId} failed: {instantAction.Message}");
|
||||
return new(false, "Request failed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"RobotManager API: Send instant action for robot {model.RobotId}, action type {model.Action.ActionType} error: {ex.Message}");
|
||||
return new(false, "An error occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("State/{robotId}")]
|
||||
public async Task<MessageResult<RobotData>> GetState(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robot = await RobotService.GetByRobotIdAsync(robotId);
|
||||
if (robot is null) return new(false, null, "RobotId does not exist.");
|
||||
if (robot.MapId is null || robot.MapId == Guid.Empty) return new(false, null, "The robot has not been assigned a map.");
|
||||
|
||||
var robotController = RobotManager.GetRobotController(robotId);
|
||||
if (robotController is null || !robotController.IsOnline) return new(false, null, "The robot is not online.");
|
||||
if (robotController.Data is null || robotController.Data.State is null) return new(false, null, "The robot is broken connection.");
|
||||
|
||||
return new(true, robotController.Data, "");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"RobotManager API: GetState for robot {robotId} error: {ex.Message}");
|
||||
return new(false, null, "An error occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("OnlineStatus/{robotId}")]
|
||||
public async Task<MessageResult<bool>> GetOnlineStatus(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robot = await RobotService.GetByRobotIdAsync(robotId);
|
||||
if (robot is null) return new(false, false, "RobotId does not exist.");
|
||||
|
||||
var robotController = RobotManager.GetRobotController(robotId);
|
||||
var isOnline = robotController?.IsOnline ?? false;
|
||||
|
||||
return new(true, isOnline, "");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"RobotManager API: GetOnlineStatus for robot {robotId} error: {ex.Message}");
|
||||
return new(false, false, "An error occurred.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.RobotModel;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Responses;
|
||||
|
||||
namespace RobotNet10.FleetManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// API controller for managing robot models.
|
||||
/// Provides RESTful endpoints for CRUD operations on robot models.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All endpoints return appropriate HTTP status codes and error responses.
|
||||
/// Image operations are handled separately via RobotModelImagesController.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("api/robot-models")]
|
||||
public class RobotModelController(IRobotModelService robotModelService, Services.Logger<RobotModelController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IRobotModelService _robotModelService = robotModelService;
|
||||
private readonly Services.Logger<RobotModelController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Get all robot models
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<RobotModelDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<RobotModelDto>>> GetAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
var models = await _robotModelService.GetAllAsync();
|
||||
var dtos = models.Select(m => MapToDto(m)).ToList();
|
||||
return Ok(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting all robot models: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while retrieving robot models." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot model by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(RobotModelDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RobotModelDto>> GetById(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = await _robotModelService.GetByIdAsync(id);
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Robot model with ID {id} not found." });
|
||||
}
|
||||
|
||||
return Ok(MapToDto(model));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting robot model {id}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while retrieving the robot model." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search robot models by query string
|
||||
/// </summary>
|
||||
[HttpGet("search")]
|
||||
[ProducesResponseType(typeof(List<RobotModelDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<RobotModelDto>>> Search([FromQuery] string query)
|
||||
{
|
||||
try
|
||||
{
|
||||
var models = await _robotModelService.SearchAsync(query);
|
||||
var dtos = models.Select(m => MapToDto(m)).ToList();
|
||||
return Ok(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error searching robot models with query '{query}': {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while searching robot models." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get usage information for a robot model
|
||||
/// </summary>
|
||||
[HttpGet("{id}/usage")]
|
||||
[ProducesResponseType(typeof(RobotModelUsageInfoDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RobotModelUsageInfoDto>> GetUsageInfo(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var usageInfo = await _robotModelService.GetUsageInfoAsync(id);
|
||||
return Ok(usageInfo);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting usage info for robot model {id}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while retrieving usage information." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new robot model
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(RobotModelDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<RobotModelDto>> Create([FromBody] CreateRobotModelRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto
|
||||
{
|
||||
Error = "Validation failed",
|
||||
Details = ModelState.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => (object)(kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() ?? Array.Empty<string>()))
|
||||
});
|
||||
}
|
||||
|
||||
var model = await _robotModelService.CreateAsync(request);
|
||||
var dto = MapToDto(model);
|
||||
return CreatedAtAction(nameof(GetById), new { id = model.Id }, dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error creating robot model: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while creating the robot model." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing robot model
|
||||
/// </summary>
|
||||
[HttpPut("{id}")]
|
||||
[ProducesResponseType(typeof(RobotModelDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<RobotModelDto>> Update(Guid id, [FromBody] UpdateRobotModelRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto
|
||||
{
|
||||
Error = "Validation failed",
|
||||
Details = ModelState.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => (object)(kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() ?? Array.Empty<string>()))
|
||||
});
|
||||
}
|
||||
|
||||
var model = await _robotModelService.UpdateAsync(id, request);
|
||||
var dto = MapToDto(model);
|
||||
return Ok(dto);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error updating robot model {id}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while updating the robot model." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a robot model
|
||||
/// </summary>
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _robotModelService.DeleteAsync(id);
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Robot model with ID {id} not found." });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error deleting robot model {id}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while deleting the robot model." });
|
||||
}
|
||||
}
|
||||
|
||||
private static RobotModelDto MapToDto(RobotModel model)
|
||||
{
|
||||
return new RobotModelDto
|
||||
{
|
||||
Id = model.Id,
|
||||
ModelName = model.ModelName,
|
||||
Length = model.Length,
|
||||
Width = model.Width,
|
||||
ImageWidth = model.ImageWidth,
|
||||
ImageHeight = model.ImageHeight,
|
||||
NavigationPointX = model.NavigationPointX,
|
||||
NavigationPointY = model.NavigationPointY,
|
||||
NavigationType = model.NavigationType,
|
||||
VehicleTypeId = model.VehicleTypeId,
|
||||
CreatedDate = model.CreatedDate,
|
||||
UpdatedDate = model.UpdatedDate,
|
||||
RobotCount = model.Robots?.Count ?? 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Responses;
|
||||
|
||||
namespace RobotNet10.FleetManager.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// API controller for managing robot model images
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/robot-models/{robotModelId}/image")]
|
||||
public class RobotModelImagesController(
|
||||
IRobotModelImageStorageService imageStorageService,
|
||||
IRobotModelService robotModelService,
|
||||
Services.Logger<RobotModelImagesController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IRobotModelImageStorageService _imageStorageService = imageStorageService;
|
||||
private readonly IRobotModelService _robotModelService = robotModelService;
|
||||
private readonly Services.Logger<RobotModelImagesController> _logger = logger;
|
||||
|
||||
private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
/// <summary>
|
||||
/// Get robot model image
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetImage(Guid robotModelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify robot model exists
|
||||
var model = await _robotModelService.GetByIdAsync(robotModelId);
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Robot model with ID {robotModelId} not found." });
|
||||
}
|
||||
|
||||
var imageStream = await _imageStorageService.GetImageAsync(robotModelId);
|
||||
if (imageStream == null)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Image for robot model {robotModelId} not found." });
|
||||
}
|
||||
|
||||
return File(imageStream, "image/png", $"{robotModelId}.png");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting image for robot model {robotModelId}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while retrieving the image." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload robot model image
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[RequestSizeLimit(MaxFileSize)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UploadImage(Guid robotModelId, IFormFile file)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify robot model exists
|
||||
var model = await _robotModelService.GetByIdAsync(robotModelId);
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Robot model with ID {robotModelId} not found." });
|
||||
}
|
||||
|
||||
// Validate file
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = "No file provided." });
|
||||
}
|
||||
|
||||
if (file.Length > MaxFileSize)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = $"File size exceeds maximum allowed size of {MaxFileSize / (1024 * 1024)}MB." });
|
||||
}
|
||||
|
||||
// Validate file extension
|
||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
if (extension != ".png")
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = "Only PNG files are allowed." });
|
||||
}
|
||||
|
||||
// Read image dimensions
|
||||
using (var stream = file.OpenReadStream())
|
||||
{
|
||||
var (width, height) = await _imageStorageService.GetImageDimensionsAsync(stream);
|
||||
|
||||
// Save image
|
||||
stream.Position = 0;
|
||||
await _imageStorageService.SaveImageAsync(robotModelId, stream);
|
||||
|
||||
// Update robot model with image dimensions
|
||||
await _robotModelService.UpdateAsync(robotModelId, new Shared.DTOs.RobotModel.UpdateRobotModelRequest
|
||||
{
|
||||
ImageWidth = width,
|
||||
ImageHeight = height
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { Error = "Image uploaded successfully." });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error uploading image for robot model {robotModelId}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while uploading the image." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete robot model image
|
||||
/// </summary>
|
||||
[HttpDelete]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> DeleteImage(Guid robotModelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify robot model exists
|
||||
var model = await _robotModelService.GetByIdAsync(robotModelId);
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Robot model with ID {robotModelId} not found." });
|
||||
}
|
||||
|
||||
var deleted = await _imageStorageService.DeleteImageAsync(robotModelId);
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound(new ErrorResponseDto { Error = $"Image for robot model {robotModelId} not found." });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error deleting image for robot model {robotModelId}: {ex.Message}");
|
||||
return StatusCode(500, new ErrorResponseDto { Error = "An error occurred while deleting the image." });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace RobotNet10.FleetManager.Data
|
||||
{
|
||||
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options)
|
||||
{
|
||||
/// <summary>
|
||||
/// Robot models
|
||||
/// </summary>
|
||||
public DbSet<RobotModel> RobotModels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Robots
|
||||
/// </summary>
|
||||
public DbSet<Robot> Robots { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Configure RobotModel
|
||||
modelBuilder.Entity<RobotModel>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.ModelName).HasDatabaseName("IX_RobotModels_ModelName");
|
||||
entity.HasIndex(e => e.NavigationType).HasDatabaseName("IX_RobotModels_NavigationType");
|
||||
entity.HasIndex(e => e.VehicleTypeId).HasDatabaseName("IX_RobotModels_VehicleTypeId");
|
||||
|
||||
entity.Property(e => e.ModelName).IsRequired().HasMaxLength(256);
|
||||
entity.Property(e => e.Length).HasPrecision(18, 2);
|
||||
entity.Property(e => e.Width).HasPrecision(18, 2);
|
||||
entity.Property(e => e.NavigationPointX).HasPrecision(18, 2);
|
||||
entity.Property(e => e.NavigationPointY).HasPrecision(18, 2);
|
||||
entity.Property(e => e.CreatedDate).IsRequired();
|
||||
});
|
||||
|
||||
// Configure Robot
|
||||
modelBuilder.Entity<Robot>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.RobotId).IsUnique().HasDatabaseName("UX_Robots_RobotId");
|
||||
entity.HasIndex(e => e.ModelId).HasDatabaseName("IX_Robots_ModelId");
|
||||
entity.HasIndex(e => e.MapId).HasDatabaseName("IX_Robots_MapId");
|
||||
|
||||
entity.Property(e => e.RobotId).IsRequired().HasMaxLength(64);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(256);
|
||||
entity.Property(e => e.ModelId).IsRequired();
|
||||
entity.Property(e => e.CreatedDate).IsRequired();
|
||||
|
||||
// Foreign key relationship
|
||||
entity.HasOne(e => e.Model)
|
||||
.WithMany(m => m.Robots)
|
||||
.HasForeignKey(e => e.ModelId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace RobotNet10.FleetManager.Data;
|
||||
|
||||
public static class ApplicationDbExtensions
|
||||
{
|
||||
extension(IServiceProvider serviceProvider)
|
||||
{
|
||||
public async Task SeedApplicationDbAsync()
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
|
||||
using var appDb = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
|
||||
await appDb.Database.MigrateAsync();
|
||||
await appDb.Database.EnsureCreatedAsync();
|
||||
await appDb.SaveChangesAsync();
|
||||
|
||||
await scope.ServiceProvider.SeedRolesAsync();
|
||||
await scope.ServiceProvider.SeedUsersAsync();
|
||||
}
|
||||
|
||||
private async Task SeedRolesAsync()
|
||||
{
|
||||
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
|
||||
|
||||
if (!await roleManager.RoleExistsAsync("Administrator"))
|
||||
{
|
||||
await roleManager.CreateAsync(new IdentityRole()
|
||||
{
|
||||
Name = "Administrator",
|
||||
NormalizedName = "ADMINISTRATOR",
|
||||
});
|
||||
}
|
||||
|
||||
if (!await roleManager.RoleExistsAsync("Distributor"))
|
||||
{
|
||||
await roleManager.CreateAsync(new IdentityRole()
|
||||
{
|
||||
Name = "Distributor",
|
||||
NormalizedName = "DISTRIBUTOR",
|
||||
});
|
||||
}
|
||||
|
||||
if (!await roleManager.RoleExistsAsync("Operator"))
|
||||
{
|
||||
await roleManager.CreateAsync(new IdentityRole()
|
||||
{
|
||||
Name = "Operator",
|
||||
NormalizedName = "OPERATOR",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SeedUsersAsync()
|
||||
{
|
||||
using var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
if (await userManager.FindByNameAsync("admin") is null)
|
||||
{
|
||||
var admin = new ApplicationUser()
|
||||
{
|
||||
UserName = "admin",
|
||||
Email = "administrator@phenikaa-x.com",
|
||||
NormalizedUserName = "ADMINISTRATOR",
|
||||
NormalizedEmail = "ADMINISTRATOR@PHENIKAA-X.COM",
|
||||
EmailConfirmed = true,
|
||||
};
|
||||
|
||||
await userManager.CreateAsync(admin, "robotics");
|
||||
await userManager.AddToRoleAsync(admin, "Administrator");
|
||||
}
|
||||
|
||||
if (await userManager.FindByNameAsync("distributor") is null)
|
||||
{
|
||||
var admin = new ApplicationUser()
|
||||
{
|
||||
UserName = "distributor",
|
||||
Email = "distributor@phenikaa-x.com",
|
||||
NormalizedUserName = "DISTRIBUTOR",
|
||||
NormalizedEmail = "DISTRIBUTOR@PHENIKAA-X.COM",
|
||||
EmailConfirmed = true,
|
||||
};
|
||||
|
||||
await userManager.CreateAsync(admin, "distributor");
|
||||
await userManager.AddToRoleAsync(admin, "Distributor");
|
||||
}
|
||||
|
||||
if (await userManager.FindByNameAsync("Operator") is null)
|
||||
{
|
||||
var admin = new ApplicationUser()
|
||||
{
|
||||
UserName = "Operator",
|
||||
Email = "Operator@phenikaa-x.com",
|
||||
NormalizedUserName = "OPERATOR",
|
||||
NormalizedEmail = "OPERATOR@PHENIKAA-X.COM",
|
||||
EmailConfirmed = true,
|
||||
};
|
||||
|
||||
await userManager.CreateAsync(admin, "Operator");
|
||||
await userManager.AddToRoleAsync(admin, "Operator");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace RobotNet10.FleetManager.Data
|
||||
{
|
||||
// Add profile data for application users by adding properties to the ApplicationUser class
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
// <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 RobotNet10.FleetManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.AppDb
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260209025755_AddAppDb")]
|
||||
partial class AddAppDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.Robot", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("MapId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("ModelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("RobotId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MapId")
|
||||
.HasDatabaseName("IX_Robots_MapId");
|
||||
|
||||
b.HasIndex("ModelId")
|
||||
.HasDatabaseName("IX_Robots_ModelId");
|
||||
|
||||
b.HasIndex("RobotId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Robots_RobotId");
|
||||
|
||||
b.ToTable("Robots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.RobotModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double>("Length")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("float(18)");
|
||||
|
||||
b.Property<string>("ModelName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<double>("NavigationPointX")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("float(18)");
|
||||
|
||||
b.Property<double>("NavigationPointY")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("float(18)");
|
||||
|
||||
b.Property<int>("NavigationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("UpdatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("VehicleTypeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double>("Width")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("float(18)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ModelName")
|
||||
.HasDatabaseName("IX_RobotModels_ModelName");
|
||||
|
||||
b.HasIndex("NavigationType")
|
||||
.HasDatabaseName("IX_RobotModels_NavigationType");
|
||||
|
||||
b.HasIndex("VehicleTypeId")
|
||||
.HasDatabaseName("IX_RobotModels_VehicleTypeId");
|
||||
|
||||
b.ToTable("RobotModels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.FleetManager.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.FleetManager.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.FleetManager.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.FleetManager.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.Robot", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.FleetManager.Data.RobotModel", "Model")
|
||||
.WithMany("Robots")
|
||||
.HasForeignKey("ModelId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Model");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.RobotModel", b =>
|
||||
{
|
||||
b.Navigation("Robots");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.AppDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAppDb : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RobotModels",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ModelName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Length = table.Column<double>(type: "float(18)", precision: 18, scale: 2, nullable: false),
|
||||
Width = table.Column<double>(type: "float(18)", precision: 18, scale: 2, nullable: false),
|
||||
ImageWidth = table.Column<int>(type: "int", nullable: false),
|
||||
ImageHeight = table.Column<int>(type: "int", nullable: false),
|
||||
NavigationPointX = table.Column<double>(type: "float(18)", precision: 18, scale: 2, nullable: false),
|
||||
NavigationPointY = table.Column<double>(type: "float(18)", precision: 18, scale: 2, nullable: false),
|
||||
NavigationType = table.Column<int>(type: "int", nullable: false),
|
||||
VehicleTypeId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedDate = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RobotModels", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Value = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Robots",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
RobotId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
ModelId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
MapId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedDate = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Robots", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Robots_RobotModels_ModelId",
|
||||
column: x => x.ModelId,
|
||||
principalTable: "RobotModels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true,
|
||||
filter: "[NormalizedName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true,
|
||||
filter: "[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RobotModels_ModelName",
|
||||
table: "RobotModels",
|
||||
column: "ModelName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RobotModels_NavigationType",
|
||||
table: "RobotModels",
|
||||
column: "NavigationType");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RobotModels_VehicleTypeId",
|
||||
table: "RobotModels",
|
||||
column: "VehicleTypeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Robots_MapId",
|
||||
table: "Robots",
|
||||
column: "MapId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Robots_ModelId",
|
||||
table: "Robots",
|
||||
column: "ModelId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_Robots_RobotId",
|
||||
table: "Robots",
|
||||
column: "RobotId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Robots");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RobotModels");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.AppDb
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.Robot", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("MapId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("ModelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("RobotId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MapId")
|
||||
.HasDatabaseName("IX_Robots_MapId");
|
||||
|
||||
b.HasIndex("ModelId")
|
||||
.HasDatabaseName("IX_Robots_ModelId");
|
||||
|
||||
b.HasIndex("RobotId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_Robots_RobotId");
|
||||
|
||||
b.ToTable("Robots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.RobotModel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double>("Length")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("float(18)");
|
||||
|
||||
b.Property<string>("ModelName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<double>("NavigationPointX")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("float(18)");
|
||||
|
||||
b.Property<double>("NavigationPointY")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("float(18)");
|
||||
|
||||
b.Property<int>("NavigationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("UpdatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("VehicleTypeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double>("Width")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("float(18)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ModelName")
|
||||
.HasDatabaseName("IX_RobotModels_ModelName");
|
||||
|
||||
b.HasIndex("NavigationType")
|
||||
.HasDatabaseName("IX_RobotModels_NavigationType");
|
||||
|
||||
b.HasIndex("VehicleTypeId")
|
||||
.HasDatabaseName("IX_RobotModels_VehicleTypeId");
|
||||
|
||||
b.ToTable("RobotModels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.FleetManager.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.FleetManager.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.FleetManager.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.FleetManager.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.Robot", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.FleetManager.Data.RobotModel", "Model")
|
||||
.WithMany("Robots")
|
||||
.HasForeignKey("ModelId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Model");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.FleetManager.Data.RobotModel", b =>
|
||||
{
|
||||
b.Navigation("Robots");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
// <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 RobotNet10.MapManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.MapDb
|
||||
{
|
||||
[DbContext(typeof(MapDbContext))]
|
||||
[Migration("20260305070805_AddMapDb")]
|
||||
partial class AddMapDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Edge", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("EdgeDescription")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("EdgeId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("EdgeName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<Guid>("EndNodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("LevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("StartNodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EdgeId");
|
||||
|
||||
b.HasIndex("EndNodeId");
|
||||
|
||||
b.HasIndex("StartNodeId");
|
||||
|
||||
b.HasIndex("LevelId", "EdgeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Edges", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_Edges_DifferentNodes", "[StartNodeId] <> [EndNodeId]");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.EdgeVehicleProperty", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Actions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<double?>("CorridorLeftWidth")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int?>("CorridorRefPoint")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("CorridorRightWidth")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("EdgeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LoadRestriction_LoadSetNames")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool?>("LoadRestriction_Loaded")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("LoadRestriction_Unloaded")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<double?>("MaxHeight")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("MaxRotationSpeed")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("MaxSpeed")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("MinHeight")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int?>("OrientationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool?>("RotationAllowed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int?>("RotationAtEndNodeAllowed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("RotationAtStartNodeAllowed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("TrajectoryControlPoint1X")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("TrajectoryControlPoint1Y")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("TrajectoryControlPoint2X")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("TrajectoryControlPoint2Y")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int?>("TrajectoryDegree")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("VehicleOrientation")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("VehicleTypeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EdgeId");
|
||||
|
||||
b.HasIndex("VehicleTypeId");
|
||||
|
||||
b.HasIndex("EdgeId", "VehicleTypeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("EdgeVehicleProperties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Layout", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("LayoutId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("LayoutName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("ModifiedBy")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("ModifiedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("LayoutId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Layouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LayoutLevelId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<int>("LevelOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("VersionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LevelOrder");
|
||||
|
||||
b.HasIndex("VersionId", "LayoutLevelId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LayoutLevels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevelEditorSettings", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double?>("BoundsMaxX")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("BoundsMaxY")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("BoundsMinX")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("BoundsMinY")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<double>("EdgeMinLengthCreate")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<bool>("EdgeNameAutoGenerate")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<double?>("ImageHeight")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("ImageWidth")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("LevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("ModifiedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("NodeNameAutoGenerate")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<double>("NodeProximityRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("OriginX")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("OriginY")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("Resolution")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LevelId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LayoutLevelEditorSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutVersion", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("LayoutDescription")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("LayoutId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("LayoutId", "Version")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LayoutVersions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Node", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("LevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("MapId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("NodeDescription")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("NodeId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("NodeName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<double>("X")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("Y")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MapId");
|
||||
|
||||
b.HasIndex("NodeId");
|
||||
|
||||
b.HasIndex("LevelId", "NodeId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("X", "Y");
|
||||
|
||||
b.ToTable("Nodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.NodeVehicleProperty", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Actions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<double?>("AllowedDeviationTheta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("AllowedDeviationXY")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double?>("Theta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("VehicleTypeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NodeId");
|
||||
|
||||
b.HasIndex("VehicleTypeId");
|
||||
|
||||
b.HasIndex("NodeId", "VehicleTypeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("NodeVehicleProperties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Station", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("LevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("StationDescription")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<double?>("StationHeight")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("StationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("StationName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<double?>("Theta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("X")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("Y")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StationId");
|
||||
|
||||
b.HasIndex("LevelId", "StationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Stations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.StationInteractionNode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("StationId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NodeId");
|
||||
|
||||
b.HasIndex("StationId");
|
||||
|
||||
b.HasIndex("StationId", "NodeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StationInteractionNodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.VehicleType", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Actions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Specifications")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("VehicleTypeId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("VehicleTypeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("VehicleTypeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("VehicleTypes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Edge", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Node", "EndNode")
|
||||
.WithMany("IncomingEdges")
|
||||
.HasForeignKey("EndNodeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutLevel", "Level")
|
||||
.WithMany("Edges")
|
||||
.HasForeignKey("LevelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.Node", "StartNode")
|
||||
.WithMany("OutgoingEdges")
|
||||
.HasForeignKey("StartNodeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("EndNode");
|
||||
|
||||
b.Navigation("Level");
|
||||
|
||||
b.Navigation("StartNode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.EdgeVehicleProperty", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Edge", "Edge")
|
||||
.WithMany("VehicleProperties")
|
||||
.HasForeignKey("EdgeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.VehicleType", "VehicleType")
|
||||
.WithMany("EdgeVehicleProperties")
|
||||
.HasForeignKey("VehicleTypeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Edge");
|
||||
|
||||
b.Navigation("VehicleType");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevel", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutVersion", "Version")
|
||||
.WithMany("Levels")
|
||||
.HasForeignKey("VersionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Version");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevelEditorSettings", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutLevel", "Level")
|
||||
.WithOne("EditorSettings")
|
||||
.HasForeignKey("RobotNet10.MapManager.Data.LayoutLevelEditorSettings", "LevelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Level");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutVersion", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Layout", "Layout")
|
||||
.WithMany("Versions")
|
||||
.HasForeignKey("LayoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Layout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Node", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutLevel", "Level")
|
||||
.WithMany("Nodes")
|
||||
.HasForeignKey("LevelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Level");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.NodeVehicleProperty", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Node", "Node")
|
||||
.WithMany("VehicleProperties")
|
||||
.HasForeignKey("NodeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.VehicleType", "VehicleType")
|
||||
.WithMany("NodeVehicleProperties")
|
||||
.HasForeignKey("VehicleTypeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Node");
|
||||
|
||||
b.Navigation("VehicleType");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Station", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutLevel", "Level")
|
||||
.WithMany("Stations")
|
||||
.HasForeignKey("LevelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Level");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.StationInteractionNode", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Node", "Node")
|
||||
.WithMany("StationInteractions")
|
||||
.HasForeignKey("NodeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.Station", "Station")
|
||||
.WithMany("InteractionNodes")
|
||||
.HasForeignKey("StationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Node");
|
||||
|
||||
b.Navigation("Station");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Edge", b =>
|
||||
{
|
||||
b.Navigation("VehicleProperties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Layout", b =>
|
||||
{
|
||||
b.Navigation("Versions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevel", b =>
|
||||
{
|
||||
b.Navigation("Edges");
|
||||
|
||||
b.Navigation("EditorSettings");
|
||||
|
||||
b.Navigation("Nodes");
|
||||
|
||||
b.Navigation("Stations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutVersion", b =>
|
||||
{
|
||||
b.Navigation("Levels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Node", b =>
|
||||
{
|
||||
b.Navigation("IncomingEdges");
|
||||
|
||||
b.Navigation("OutgoingEdges");
|
||||
|
||||
b.Navigation("StationInteractions");
|
||||
|
||||
b.Navigation("VehicleProperties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Station", b =>
|
||||
{
|
||||
b.Navigation("InteractionNodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.VehicleType", b =>
|
||||
{
|
||||
b.Navigation("EdgeVehicleProperties");
|
||||
|
||||
b.Navigation("NodeVehicleProperties");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.MapDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMapDb : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Layouts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LayoutId = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
LayoutName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ModifiedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ModifiedBy = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Layouts", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "VehicleTypes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
VehicleTypeId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
VehicleTypeName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Specifications = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
Actions = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_VehicleTypes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LayoutVersions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LayoutId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Version = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
|
||||
LayoutDescription = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LayoutVersions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_LayoutVersions_Layouts_LayoutId",
|
||||
column: x => x.LayoutId,
|
||||
principalTable: "Layouts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LayoutLevels",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
VersionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LayoutLevelId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
LevelOrder = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LayoutLevels", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_LayoutLevels_LayoutVersions_VersionId",
|
||||
column: x => x.VersionId,
|
||||
principalTable: "LayoutVersions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LayoutLevelEditorSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LevelId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
EdgeMinLengthCreate = table.Column<double>(type: "float", nullable: false),
|
||||
EdgeNameAutoGenerate = table.Column<bool>(type: "bit", nullable: false),
|
||||
NodeNameAutoGenerate = table.Column<bool>(type: "bit", nullable: false),
|
||||
NodeProximityRadius = table.Column<double>(type: "float", nullable: false),
|
||||
OriginX = table.Column<double>(type: "float", nullable: false),
|
||||
OriginY = table.Column<double>(type: "float", nullable: false),
|
||||
Resolution = table.Column<double>(type: "float", nullable: false),
|
||||
BoundsMinX = table.Column<double>(type: "float", nullable: true),
|
||||
BoundsMaxX = table.Column<double>(type: "float", nullable: true),
|
||||
BoundsMinY = table.Column<double>(type: "float", nullable: true),
|
||||
BoundsMaxY = table.Column<double>(type: "float", nullable: true),
|
||||
ImageWidth = table.Column<double>(type: "float", nullable: true),
|
||||
ImageHeight = table.Column<double>(type: "float", nullable: true),
|
||||
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ModifiedDate = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LayoutLevelEditorSettings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_LayoutLevelEditorSettings_LayoutLevels_LevelId",
|
||||
column: x => x.LevelId,
|
||||
principalTable: "LayoutLevels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Nodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LevelId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
NodeId = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
NodeName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NodeDescription = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
MapId = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
X = table.Column<double>(type: "float", nullable: false),
|
||||
Y = table.Column<double>(type: "float", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Nodes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Nodes_LayoutLevels_LevelId",
|
||||
column: x => x.LevelId,
|
||||
principalTable: "LayoutLevels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Stations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LevelId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
StationId = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
StationName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
StationDescription = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
StationHeight = table.Column<double>(type: "float", nullable: true),
|
||||
X = table.Column<double>(type: "float", nullable: false),
|
||||
Y = table.Column<double>(type: "float", nullable: false),
|
||||
Theta = table.Column<double>(type: "float", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Stations", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Stations_LayoutLevels_LevelId",
|
||||
column: x => x.LevelId,
|
||||
principalTable: "LayoutLevels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Edges",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LevelId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
EdgeId = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
StartNodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
EndNodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
EdgeName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
EdgeDescription = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Edges", x => x.Id);
|
||||
table.CheckConstraint("CK_Edges_DifferentNodes", "[StartNodeId] <> [EndNodeId]");
|
||||
table.ForeignKey(
|
||||
name: "FK_Edges_LayoutLevels_LevelId",
|
||||
column: x => x.LevelId,
|
||||
principalTable: "LayoutLevels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Edges_Nodes_EndNodeId",
|
||||
column: x => x.EndNodeId,
|
||||
principalTable: "Nodes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Edges_Nodes_StartNodeId",
|
||||
column: x => x.StartNodeId,
|
||||
principalTable: "Nodes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NodeVehicleProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
NodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
VehicleTypeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Theta = table.Column<double>(type: "float", nullable: true),
|
||||
Actions = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
AllowedDeviationXY = table.Column<double>(type: "float", nullable: true),
|
||||
AllowedDeviationTheta = table.Column<double>(type: "float", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NodeVehicleProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_NodeVehicleProperties_Nodes_NodeId",
|
||||
column: x => x.NodeId,
|
||||
principalTable: "Nodes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_NodeVehicleProperties_VehicleTypes_VehicleTypeId",
|
||||
column: x => x.VehicleTypeId,
|
||||
principalTable: "VehicleTypes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StationInteractionNodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
StationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
NodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StationInteractionNodes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_StationInteractionNodes_Nodes_NodeId",
|
||||
column: x => x.NodeId,
|
||||
principalTable: "Nodes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_StationInteractionNodes_Stations_StationId",
|
||||
column: x => x.StationId,
|
||||
principalTable: "Stations",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "EdgeVehicleProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
EdgeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
VehicleTypeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
VehicleOrientation = table.Column<double>(type: "float", nullable: true),
|
||||
OrientationType = table.Column<int>(type: "int", nullable: true),
|
||||
RotationAllowed = table.Column<bool>(type: "bit", nullable: true),
|
||||
RotationAtStartNodeAllowed = table.Column<int>(type: "int", nullable: true),
|
||||
RotationAtEndNodeAllowed = table.Column<int>(type: "int", nullable: true),
|
||||
MaxSpeed = table.Column<double>(type: "float", nullable: true),
|
||||
MaxRotationSpeed = table.Column<double>(type: "float", nullable: true),
|
||||
MinHeight = table.Column<double>(type: "float", nullable: true),
|
||||
MaxHeight = table.Column<double>(type: "float", nullable: true),
|
||||
LoadRestriction_Unloaded = table.Column<bool>(type: "bit", nullable: true),
|
||||
LoadRestriction_Loaded = table.Column<bool>(type: "bit", nullable: true),
|
||||
LoadRestriction_LoadSetNames = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
TrajectoryDegree = table.Column<int>(type: "int", nullable: true),
|
||||
TrajectoryControlPoint1X = table.Column<double>(type: "float", nullable: true),
|
||||
TrajectoryControlPoint1Y = table.Column<double>(type: "float", nullable: true),
|
||||
TrajectoryControlPoint2X = table.Column<double>(type: "float", nullable: true),
|
||||
TrajectoryControlPoint2Y = table.Column<double>(type: "float", nullable: true),
|
||||
Actions = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CorridorLeftWidth = table.Column<double>(type: "float", nullable: true),
|
||||
CorridorRightWidth = table.Column<double>(type: "float", nullable: true),
|
||||
CorridorRefPoint = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_EdgeVehicleProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_EdgeVehicleProperties_Edges_EdgeId",
|
||||
column: x => x.EdgeId,
|
||||
principalTable: "Edges",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_EdgeVehicleProperties_VehicleTypes_VehicleTypeId",
|
||||
column: x => x.VehicleTypeId,
|
||||
principalTable: "VehicleTypes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Edges_EdgeId",
|
||||
table: "Edges",
|
||||
column: "EdgeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Edges_EndNodeId",
|
||||
table: "Edges",
|
||||
column: "EndNodeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Edges_LevelId_EdgeId",
|
||||
table: "Edges",
|
||||
columns: new[] { "LevelId", "EdgeId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Edges_StartNodeId",
|
||||
table: "Edges",
|
||||
column: "StartNodeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EdgeVehicleProperties_EdgeId",
|
||||
table: "EdgeVehicleProperties",
|
||||
column: "EdgeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EdgeVehicleProperties_EdgeId_VehicleTypeId",
|
||||
table: "EdgeVehicleProperties",
|
||||
columns: new[] { "EdgeId", "VehicleTypeId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EdgeVehicleProperties_VehicleTypeId",
|
||||
table: "EdgeVehicleProperties",
|
||||
column: "VehicleTypeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LayoutLevelEditorSettings_LevelId",
|
||||
table: "LayoutLevelEditorSettings",
|
||||
column: "LevelId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LayoutLevels_LevelOrder",
|
||||
table: "LayoutLevels",
|
||||
column: "LevelOrder");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LayoutLevels_VersionId_LayoutLevelId",
|
||||
table: "LayoutLevels",
|
||||
columns: new[] { "VersionId", "LayoutLevelId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Layouts_IsActive",
|
||||
table: "Layouts",
|
||||
column: "IsActive");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Layouts_LayoutId",
|
||||
table: "Layouts",
|
||||
column: "LayoutId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LayoutVersions_IsActive",
|
||||
table: "LayoutVersions",
|
||||
column: "IsActive");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LayoutVersions_LayoutId_Version",
|
||||
table: "LayoutVersions",
|
||||
columns: new[] { "LayoutId", "Version" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Nodes_LevelId_NodeId",
|
||||
table: "Nodes",
|
||||
columns: new[] { "LevelId", "NodeId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Nodes_MapId",
|
||||
table: "Nodes",
|
||||
column: "MapId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Nodes_NodeId",
|
||||
table: "Nodes",
|
||||
column: "NodeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Nodes_X_Y",
|
||||
table: "Nodes",
|
||||
columns: new[] { "X", "Y" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NodeVehicleProperties_NodeId",
|
||||
table: "NodeVehicleProperties",
|
||||
column: "NodeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NodeVehicleProperties_NodeId_VehicleTypeId",
|
||||
table: "NodeVehicleProperties",
|
||||
columns: new[] { "NodeId", "VehicleTypeId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NodeVehicleProperties_VehicleTypeId",
|
||||
table: "NodeVehicleProperties",
|
||||
column: "VehicleTypeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StationInteractionNodes_NodeId",
|
||||
table: "StationInteractionNodes",
|
||||
column: "NodeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StationInteractionNodes_StationId",
|
||||
table: "StationInteractionNodes",
|
||||
column: "StationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StationInteractionNodes_StationId_NodeId",
|
||||
table: "StationInteractionNodes",
|
||||
columns: new[] { "StationId", "NodeId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Stations_LevelId_StationId",
|
||||
table: "Stations",
|
||||
columns: new[] { "LevelId", "StationId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Stations_StationId",
|
||||
table: "Stations",
|
||||
column: "StationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VehicleTypes_IsActive",
|
||||
table: "VehicleTypes",
|
||||
column: "IsActive");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VehicleTypes_VehicleTypeId",
|
||||
table: "VehicleTypes",
|
||||
column: "VehicleTypeId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "EdgeVehicleProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LayoutLevelEditorSettings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NodeVehicleProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "StationInteractionNodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Edges");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "VehicleTypes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Stations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Nodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LayoutLevels");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LayoutVersions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Layouts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,707 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet10.MapManager.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.MapDb
|
||||
{
|
||||
[DbContext(typeof(MapDbContext))]
|
||||
partial class MapDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Edge", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("EdgeDescription")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("EdgeId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("EdgeName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<Guid>("EndNodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("LevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("StartNodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EdgeId");
|
||||
|
||||
b.HasIndex("EndNodeId");
|
||||
|
||||
b.HasIndex("StartNodeId");
|
||||
|
||||
b.HasIndex("LevelId", "EdgeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Edges", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_Edges_DifferentNodes", "[StartNodeId] <> [EndNodeId]");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.EdgeVehicleProperty", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Actions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<double?>("CorridorLeftWidth")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int?>("CorridorRefPoint")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("CorridorRightWidth")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("EdgeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LoadRestriction_LoadSetNames")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool?>("LoadRestriction_Loaded")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool?>("LoadRestriction_Unloaded")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<double?>("MaxHeight")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("MaxRotationSpeed")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("MaxSpeed")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("MinHeight")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int?>("OrientationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool?>("RotationAllowed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int?>("RotationAtEndNodeAllowed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("RotationAtStartNodeAllowed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("TrajectoryControlPoint1X")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("TrajectoryControlPoint1Y")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("TrajectoryControlPoint2X")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("TrajectoryControlPoint2Y")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int?>("TrajectoryDegree")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("VehicleOrientation")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("VehicleTypeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EdgeId");
|
||||
|
||||
b.HasIndex("VehicleTypeId");
|
||||
|
||||
b.HasIndex("EdgeId", "VehicleTypeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("EdgeVehicleProperties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Layout", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("LayoutId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("LayoutName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("ModifiedBy")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("ModifiedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("LayoutId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Layouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LayoutLevelId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<int>("LevelOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("VersionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LevelOrder");
|
||||
|
||||
b.HasIndex("VersionId", "LayoutLevelId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LayoutLevels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevelEditorSettings", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double?>("BoundsMaxX")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("BoundsMaxY")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("BoundsMinX")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("BoundsMinY")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<double>("EdgeMinLengthCreate")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<bool>("EdgeNameAutoGenerate")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<double?>("ImageHeight")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("ImageWidth")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("LevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("ModifiedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("NodeNameAutoGenerate")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<double>("NodeProximityRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("OriginX")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("OriginY")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("Resolution")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LevelId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LayoutLevelEditorSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutVersion", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("LayoutDescription")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("LayoutId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("LayoutId", "Version")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LayoutVersions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Node", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("LevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("MapId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("NodeDescription")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("NodeId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("NodeName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<double>("X")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("Y")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MapId");
|
||||
|
||||
b.HasIndex("NodeId");
|
||||
|
||||
b.HasIndex("LevelId", "NodeId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("X", "Y");
|
||||
|
||||
b.ToTable("Nodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.NodeVehicleProperty", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Actions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<double?>("AllowedDeviationTheta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("AllowedDeviationXY")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double?>("Theta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<Guid>("VehicleTypeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NodeId");
|
||||
|
||||
b.HasIndex("VehicleTypeId");
|
||||
|
||||
b.HasIndex("NodeId", "VehicleTypeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("NodeVehicleProperties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Station", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("LevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("StationDescription")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<double?>("StationHeight")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("StationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("StationName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<double?>("Theta")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("X")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("Y")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StationId");
|
||||
|
||||
b.HasIndex("LevelId", "StationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Stations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.StationInteractionNode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("StationId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NodeId");
|
||||
|
||||
b.HasIndex("StationId");
|
||||
|
||||
b.HasIndex("StationId", "NodeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StationInteractionNodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.VehicleType", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Actions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Specifications")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("VehicleTypeId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("VehicleTypeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("VehicleTypeId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("VehicleTypes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Edge", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Node", "EndNode")
|
||||
.WithMany("IncomingEdges")
|
||||
.HasForeignKey("EndNodeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutLevel", "Level")
|
||||
.WithMany("Edges")
|
||||
.HasForeignKey("LevelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.Node", "StartNode")
|
||||
.WithMany("OutgoingEdges")
|
||||
.HasForeignKey("StartNodeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("EndNode");
|
||||
|
||||
b.Navigation("Level");
|
||||
|
||||
b.Navigation("StartNode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.EdgeVehicleProperty", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Edge", "Edge")
|
||||
.WithMany("VehicleProperties")
|
||||
.HasForeignKey("EdgeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.VehicleType", "VehicleType")
|
||||
.WithMany("EdgeVehicleProperties")
|
||||
.HasForeignKey("VehicleTypeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Edge");
|
||||
|
||||
b.Navigation("VehicleType");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevel", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutVersion", "Version")
|
||||
.WithMany("Levels")
|
||||
.HasForeignKey("VersionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Version");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevelEditorSettings", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutLevel", "Level")
|
||||
.WithOne("EditorSettings")
|
||||
.HasForeignKey("RobotNet10.MapManager.Data.LayoutLevelEditorSettings", "LevelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Level");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutVersion", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Layout", "Layout")
|
||||
.WithMany("Versions")
|
||||
.HasForeignKey("LayoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Layout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Node", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutLevel", "Level")
|
||||
.WithMany("Nodes")
|
||||
.HasForeignKey("LevelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Level");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.NodeVehicleProperty", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Node", "Node")
|
||||
.WithMany("VehicleProperties")
|
||||
.HasForeignKey("NodeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.VehicleType", "VehicleType")
|
||||
.WithMany("NodeVehicleProperties")
|
||||
.HasForeignKey("VehicleTypeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Node");
|
||||
|
||||
b.Navigation("VehicleType");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Station", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.LayoutLevel", "Level")
|
||||
.WithMany("Stations")
|
||||
.HasForeignKey("LevelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Level");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.StationInteractionNode", b =>
|
||||
{
|
||||
b.HasOne("RobotNet10.MapManager.Data.Node", "Node")
|
||||
.WithMany("StationInteractions")
|
||||
.HasForeignKey("NodeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("RobotNet10.MapManager.Data.Station", "Station")
|
||||
.WithMany("InteractionNodes")
|
||||
.HasForeignKey("StationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Node");
|
||||
|
||||
b.Navigation("Station");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Edge", b =>
|
||||
{
|
||||
b.Navigation("VehicleProperties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Layout", b =>
|
||||
{
|
||||
b.Navigation("Versions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutLevel", b =>
|
||||
{
|
||||
b.Navigation("Edges");
|
||||
|
||||
b.Navigation("EditorSettings");
|
||||
|
||||
b.Navigation("Nodes");
|
||||
|
||||
b.Navigation("Stations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.LayoutVersion", b =>
|
||||
{
|
||||
b.Navigation("Levels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Node", b =>
|
||||
{
|
||||
b.Navigation("IncomingEdges");
|
||||
|
||||
b.Navigation("OutgoingEdges");
|
||||
|
||||
b.Navigation("StationInteractions");
|
||||
|
||||
b.Navigation("VehicleProperties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.Station", b =>
|
||||
{
|
||||
b.Navigation("InteractionNodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RobotNet10.MapManager.Data.VehicleType", b =>
|
||||
{
|
||||
b.Navigation("EdgeVehicleProperties");
|
||||
|
||||
b.Navigation("NodeVehicleProperties");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// <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 RobotNet10.ScriptEngine.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.ScriptDb
|
||||
{
|
||||
[DbContext(typeof(ScriptEngineDbContext))]
|
||||
[Migration("20260209025906_AddScriptDb")]
|
||||
partial class AddScriptDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RobotNet10.ScriptEngine.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("nvarchar(max)")
|
||||
.HasColumnName("MissionName");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Parameters");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("Score");
|
||||
|
||||
b.Property<int>("State")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("State");
|
||||
|
||||
b.Property<DateTime>("StoppedAt")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("StoppedAt");
|
||||
|
||||
b.Property<int>("TotalScore")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("TotalScore");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("InstanceMissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.ScriptDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddScriptDb : 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: "nvarchar(max)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
Parameters = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
TotalScore = table.Column<int>(type: "int", nullable: false),
|
||||
State = table.Column<int>(type: "int", nullable: false),
|
||||
Score = table.Column<int>(type: "int", nullable: false),
|
||||
StoppedAt = table.Column<DateTime>(type: "datetime2", 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RobotNet10.ScriptEngine.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RobotNet10.FleetManager.Data.Migrations.ScriptDb
|
||||
{
|
||||
[DbContext(typeof(ScriptEngineDbContext))]
|
||||
partial class ScriptEngineDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RobotNet10.ScriptEngine.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("nvarchar(max)")
|
||||
.HasColumnName("MissionName");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Parameters");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("Score");
|
||||
|
||||
b.Property<int>("State")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("State");
|
||||
|
||||
b.Property<DateTime>("StoppedAt")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("StoppedAt");
|
||||
|
||||
b.Property<int>("TotalScore")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("TotalScore");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("InstanceMissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace RobotNet10.FleetManager.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Robot entity - Individual robot information
|
||||
/// </summary>
|
||||
[Table("Robots")]
|
||||
public class Robot
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Robot identifier (serial number or unique identifier)
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string RobotId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Display name of the robot
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Foreign key to RobotModel
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid ModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Foreign key to Map (optional)
|
||||
/// </summary>
|
||||
public Guid? MapId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Created date
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Updated date
|
||||
/// </summary>
|
||||
public DateTime? UpdatedDate { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
/// <summary>
|
||||
/// Robot model this robot belongs to
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(ModelId))]
|
||||
public virtual RobotModel? Model { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using RobotNet10.FleetManager.Shared.Enums;
|
||||
|
||||
namespace RobotNet10.FleetManager.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Robot model entity - Master data for robot types
|
||||
/// </summary>
|
||||
[Table("RobotModels")]
|
||||
public class RobotModel
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Model name
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
public string ModelName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Robot length in meters
|
||||
/// </summary>
|
||||
[Required]
|
||||
public double Length { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Robot width in meters
|
||||
/// </summary>
|
||||
[Required]
|
||||
public double Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Image width in pixels
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int ImageWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Image height in pixels
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int ImageHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation point X coordinate (relative to robot center) in meters
|
||||
/// </summary>
|
||||
[Required]
|
||||
public double NavigationPointX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation point Y coordinate (relative to robot center) in meters
|
||||
/// </summary>
|
||||
[Required]
|
||||
public double NavigationPointY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation type
|
||||
/// </summary>
|
||||
[Required]
|
||||
public NavigationType NavigationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Foreign key to VehicleType (in MapManager database)
|
||||
/// Links RobotModel to VehicleType for map filtering
|
||||
/// </summary>
|
||||
public Guid? VehicleTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Created date
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Updated date
|
||||
/// </summary>
|
||||
public DateTime? UpdatedDate { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
/// <summary>
|
||||
/// Collection of robots using this model
|
||||
/// </summary>
|
||||
public virtual ICollection<Robot> Robots { get; set; } = [];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "connection",
|
||||
"description": "The last will message of the AGV. Has to be sent with retain flag.\nOnce the AGV comes online, it has to send this message on its connect topic, with the connectionState enum set to \"ONLINE\".\n The last will message is to be configured with the connection state set to \"CONNECTIONBROKEN\".\nThus, if the AGV disconnects from the broker, master control gets notified via the topic \"connection\".\nIf the AGV is disconnecting in an orderly fashion (e.g. shutting down, sleeping), the AGV is to publish a message on this topic with the connectionState set to \"DISCONNECTED\".",
|
||||
"subtopic": "/connection",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"headerId",
|
||||
"timestamp",
|
||||
"version",
|
||||
"manufacturer",
|
||||
"serialNumber",
|
||||
"connectionState"
|
||||
],
|
||||
"properties": {
|
||||
"headerId": {
|
||||
"type": "integer",
|
||||
"description": "Header ID of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message."
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ssZ).",
|
||||
"examples": [
|
||||
"1991-03-11T11:40:03.12Z"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Version of the protocol [Major].[Minor].[Patch]",
|
||||
"examples": [
|
||||
"1.3.2"
|
||||
]
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"description": "Manufacturer of the AGV."
|
||||
},
|
||||
"serialNumber": {
|
||||
"type": "string",
|
||||
"description": "Serial number of the AGV."
|
||||
},
|
||||
"connectionState": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ONLINE",
|
||||
"OFFLINE",
|
||||
"CONNECTIONBROKEN"
|
||||
],
|
||||
"description": "ONLINE: connection between AGV and broker is active. OFFLINE: connection between AGV and broker has gone offline in a coordinated way. CONNECTIONBROKEN: The connection between AGV and broker has unexpectedly ended."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,831 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "AGV Factsheet",
|
||||
"description": "The factsheet provides basic information about a specific AGV type series. This information allows comparison of different AGV types and can be applied for the planning, dimensioning and simulation of an AGV system. The factsheet also includes information about AGV communication interfaces which are required for the integration of an AGV type series into a VD[M]A-5050-compliant master control.",
|
||||
"required": [
|
||||
"headerId",
|
||||
"timestamp",
|
||||
"version",
|
||||
"manufacturer",
|
||||
"serialNumber",
|
||||
"typeSpecification",
|
||||
"physicalParameters",
|
||||
"protocolLimits",
|
||||
"protocolFeatures",
|
||||
"agvGeometry",
|
||||
"loadSpecification"
|
||||
],
|
||||
"subtopic": "/factsheet",
|
||||
"type": "object",
|
||||
"properties":{
|
||||
"headerId": {
|
||||
"type": "integer",
|
||||
"description": "header ID of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message.",
|
||||
"minimum": 0
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ffZ).",
|
||||
"examples": [
|
||||
"1991-03-11T11:40:03.12Z"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Version of the VD[M]A-5050 protocol [Major].[Minor].[Patch] (e.g. 1.3.2)",
|
||||
"examples": [
|
||||
"2.0.0"
|
||||
]
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"description": "Manufacturer of the AGV"
|
||||
},
|
||||
"serialNumber": {
|
||||
"type": "string",
|
||||
"description": "Serial number of the AGV"
|
||||
},
|
||||
"typeSpecification": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"seriesName",
|
||||
"agvKinematic",
|
||||
"agvClass",
|
||||
"maxLoadMass",
|
||||
"localizationTypes",
|
||||
"navigationTypes"
|
||||
],
|
||||
"description": "These parameters generally specify the class and the capabilities of the AGV",
|
||||
"properties": {
|
||||
"seriesName": {
|
||||
"type": "string",
|
||||
"description": "Free text generalized series name as specified by manufacturer"
|
||||
},
|
||||
"seriesDescription": {
|
||||
"type": "string",
|
||||
"description": "Free text human readable description of the AGV type series"
|
||||
},
|
||||
"agvKinematic": {
|
||||
"type": "string",
|
||||
"description": "simplified description of AGV kinematics-type.",
|
||||
"enum": [
|
||||
"DIFF",
|
||||
"OMNI",
|
||||
"THREEWHEEL"
|
||||
]
|
||||
},
|
||||
"agvClass": {
|
||||
"type": "string",
|
||||
"description": "Simplified description of AGV class.",
|
||||
"enum": [
|
||||
"FORKLIFT",
|
||||
"CONVEYOR",
|
||||
"TUGGER",
|
||||
"CARRIER"
|
||||
]
|
||||
},
|
||||
"maxLoadMass": {
|
||||
"type": "number",
|
||||
"description": "maximum loadable mass",
|
||||
"unit": "kg",
|
||||
"minimum": 0
|
||||
},
|
||||
"localizationTypes": {
|
||||
"type": "array",
|
||||
"description": "simplified description of localization type",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"NATURAL",
|
||||
"REFLECTOR",
|
||||
"RFID",
|
||||
"DMC",
|
||||
"SPOT",
|
||||
"GRID"
|
||||
]
|
||||
}
|
||||
},
|
||||
"navigationTypes": {
|
||||
"type": "array",
|
||||
"description": "List of path planning types supported by the AGV, sorted by priority",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"PHYSICAL_LINE_GUIDED",
|
||||
"VIRTUAL_LINE_GUIDED",
|
||||
"AUTONOMOUS"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"physicalParameters": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"speedMin",
|
||||
"speedMax",
|
||||
"accelerationMax",
|
||||
"decelerationMax",
|
||||
"heightMax",
|
||||
"width",
|
||||
"length"
|
||||
],
|
||||
"description": "These parameters specify the basic physical properties of the AGV",
|
||||
"properties": {
|
||||
"speedMin": {
|
||||
"type": "number",
|
||||
"description": "minimal controlled continuous speed of the AGV",
|
||||
"unit": "m/s"
|
||||
},
|
||||
"speedMax": {
|
||||
"type": "number",
|
||||
"description": "maximum speed of the AGV",
|
||||
"unit": "m/s"
|
||||
},
|
||||
"accelerationMax": {
|
||||
"type": "number",
|
||||
"description": "maximum acceleration with maximum load",
|
||||
"unit": "m/s^2"
|
||||
},
|
||||
"decelerationMax": {
|
||||
"type": "number",
|
||||
"description": "maximum deceleration with maximum load",
|
||||
"unit": "m/s^2"
|
||||
},
|
||||
"heightMin": {
|
||||
"type": "number",
|
||||
"description": "minimum height of AGV",
|
||||
"unit": "m"
|
||||
},
|
||||
"heightMax": {
|
||||
"type": "number",
|
||||
"description": "maximum height of AGV",
|
||||
"unit": "m"
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"description": "width of AGV",
|
||||
"unit": "m"
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"description": "length of AGV",
|
||||
"unit": "m"
|
||||
}
|
||||
}
|
||||
},
|
||||
"protocolLimits": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"maxStringLens",
|
||||
"maxArrayLens",
|
||||
"timing"
|
||||
],
|
||||
"description": "This JSON-object describes the protocol limitations of the AGV. If a parameter is not defined or set to zero then there is no explicit limit for this parameter.",
|
||||
"properties": {
|
||||
"maxStringLens": {
|
||||
"type": "object",
|
||||
"description": "maximum lengths of strings",
|
||||
"properties": {
|
||||
"msgLen": {
|
||||
"type": "integer",
|
||||
"description": "maximum MQTT Message length"
|
||||
},
|
||||
"topicSerialLen": {
|
||||
"type": "integer",
|
||||
"description": "maximum length of serial-number part in MQTT-topics. Affected Parameters: order.serialNumber, instantActions.serialNumber, state.SerialNumber, visualization.serialNumber, connection.serialNumber"
|
||||
},
|
||||
"topicElemLen": {
|
||||
"type": "integer",
|
||||
"description": "maximum length of all other parts in MQTT-topics. Affected parameters: order.timestamp, order.version, order.manufacturer, instantActions.timestamp, instantActions.version, instantActions.manufacturer, state.timestamp, state.version, state.manufacturer, visualization.timestamp, visualization.version, visualization.manufacturer, connection.timestamp, connection.version, connection.manufacturer"
|
||||
},
|
||||
"idLen": {
|
||||
"type": "integer",
|
||||
"description": "maximum length of ID-Strings. Affected parameters: order.orderId, order.zoneSetId, node.nodeId, nodePosition.mapId, action.actionId, edge.edgeId, edge.startNodeId, edge.endNodeId"
|
||||
},
|
||||
"idNumericalOnly": {
|
||||
"type": "boolean",
|
||||
"description": "If true ID-strings need to contain numerical values only"
|
||||
},
|
||||
"enumLen": {
|
||||
"type": "integer",
|
||||
"description": "maximum length of ENUM- and Key-Strings. Affected parameters: action.actionType, action.blockingType, edge.direction, actionParameter.key, state.operatingMode, load.loadPosition, load.loadType, actionState.actionStatus, error.errorType, error.errorLevel, errorReference.referenceKey, info.infoType, info.infoLevel, safetyState.eStop, connection.connectionState"
|
||||
},
|
||||
"loadIdLen": {
|
||||
"type": "integer",
|
||||
"description": "maximum length of loadId Strings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maxArrayLens": {
|
||||
"type": "object",
|
||||
"description": "maximum lengths of arrays",
|
||||
"properties": {
|
||||
"order.nodes": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of nodes per order processable by the AGV"
|
||||
},
|
||||
"order.edges": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of edges per order processable by the AGV"
|
||||
},
|
||||
"node.actions": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of actions per node processable by the AGV"
|
||||
},
|
||||
"edge.actions": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of actions per edge processable by the AGV"
|
||||
},
|
||||
"actions.actionsParameters": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of parameters per action processable by the AGV"
|
||||
},
|
||||
"instantActions": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of instant actions per message processable by the AGV"
|
||||
},
|
||||
"trajectory.knotVector": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of knots per trajectory processable by the AGV"
|
||||
},
|
||||
"trajectory.controlPoints": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of control points per trajectory processable by the AGV"
|
||||
},
|
||||
"state.nodeStates": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of nodeStates sent by the AGV, maximum number of nodes in base of AGV"
|
||||
},
|
||||
"state.edgeStates": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of edgeStates sent by the AGV, maximum number of edges in base of AGV"
|
||||
},
|
||||
"state.loads": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of load-objects sent by the AGV"
|
||||
},
|
||||
"state.actionStates": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of actionStates sent by the AGV"
|
||||
},
|
||||
"state.errors": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of errors sent by the AGV in one state-message"
|
||||
},
|
||||
"state.information": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of information objects sent by the AGV in one state-message"
|
||||
},
|
||||
"error.errorReferences": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of error references sent by the AGV for each error"
|
||||
},
|
||||
"information.infoReferences": {
|
||||
"type": "integer",
|
||||
"description": "maximum number of info references sent by the AGV for each information"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"minOrderInterval",
|
||||
"minStateInterval"
|
||||
],
|
||||
"description": "timing information",
|
||||
"properties": {
|
||||
"minOrderInterval": {
|
||||
"type": "number",
|
||||
"description": "minimum interval sending order messages to the AGV",
|
||||
"unit": "s"
|
||||
},
|
||||
"minStateInterval": {
|
||||
"type": "number",
|
||||
"description": "minimum interval for sending state-messages",
|
||||
"unit": "s"
|
||||
},
|
||||
"defaultStateInterval": {
|
||||
"type": "number",
|
||||
"description": "default interval for sending state-messages if not defined, the default value from the main document is used",
|
||||
"unit": "s"
|
||||
},
|
||||
"visualizationInterval": {
|
||||
"type": "number",
|
||||
"description": "default interval for sending messages on visualization topic",
|
||||
"unit": "s"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"protocolFeatures": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"optionalParameters",
|
||||
"agvActions"
|
||||
],
|
||||
"description": "Supported features of VDA5050 protocol",
|
||||
"properties": {
|
||||
"optionalParameters": {
|
||||
"type": "array",
|
||||
"description": "list of supported and/or required optional parameters. Optional parameters, that are not listed here, are assumed to be not supported by the AGV.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"parameter",
|
||||
"support"
|
||||
],
|
||||
"properties": {
|
||||
"parameter": {
|
||||
"type": "string",
|
||||
"description": "full name of optional parameter, e.g. “order.nodes.nodePosition.allowedDeviationTheta”"
|
||||
},
|
||||
"support": {
|
||||
"type": "string",
|
||||
"description": "type of support for the optional parameter, the following values are possible: SUPPORTED: optional parameter is supported like specified. REQUIRED: optional parameter is required for proper AGV-operation.",
|
||||
"enum": [
|
||||
"SUPPORTED",
|
||||
"REQUIRED"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "free text. Description of optional parameter. E.g. Reason, why the optional parameter ‚direction‘ is necessary for this AGV-type and which values it can contain. The parameter ‘nodeMarker’ must contain unsigned interger-numbers only. Nurbs-Support is limited to straight lines and circle segments."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"agvActions": {
|
||||
"type": "array",
|
||||
"description": "list of all actions with parameters supported by this AGV. This includes standard actions specified in VDA5050 and manufacturer-specific actions",
|
||||
"items": {
|
||||
"required": [
|
||||
"actionType",
|
||||
"actionScopes"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actionType": {
|
||||
"type": "string",
|
||||
"description": "unique actionType corresponding to action.actionType"
|
||||
},
|
||||
"actionDescription": {
|
||||
"type": "string",
|
||||
"description": "free text: description of the action"
|
||||
},
|
||||
"actionScopes": {
|
||||
"type": "array",
|
||||
"description": "list of allowed scopes for using this action-type. INSTANT: usable as instantAction, NODE: usable on nodes, EDGE: usable on edges.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"INSTANT",
|
||||
"NODE",
|
||||
"EDGE"
|
||||
]
|
||||
}
|
||||
},
|
||||
"actionParameters": {
|
||||
"type": "array",
|
||||
"description": "list of parameters. if not defined, the action has no parameters",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key",
|
||||
"valueDataType"
|
||||
],
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "key-String for Parameter"
|
||||
},
|
||||
"valueDataType": {
|
||||
"type": "string",
|
||||
"description": "data type of Value, possible data types are: BOOL, NUMBER, INTEGER, FLOAT, STRING, OBJECT, ARRAY",
|
||||
"enum": [
|
||||
"BOOL",
|
||||
"NUMBER",
|
||||
"INTEGER",
|
||||
"FLOAT",
|
||||
"STRING",
|
||||
"OBJECT",
|
||||
"ARRAY"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "free text: description of the parameter"
|
||||
},
|
||||
"isOptional": {
|
||||
"type": "boolean",
|
||||
"description": "True: optional parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resultDescription": {
|
||||
"type": "string",
|
||||
"description": "free text: description of the resultDescription"
|
||||
},
|
||||
"blockingTypes": {
|
||||
"type": "array",
|
||||
"description": "Array of possible blocking types for defined action.",
|
||||
"items": {
|
||||
"type":"string",
|
||||
"enum": [
|
||||
"NONE",
|
||||
"SOFT",
|
||||
"HARD"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"agvGeometry": {
|
||||
"type": "object",
|
||||
"description": "Detailed definition of AGV geometry",
|
||||
"properties": {
|
||||
"wheelDefinitions": {
|
||||
"type": "array",
|
||||
"description": "list of wheels, containing wheel-arrangement and geometry",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"isActiveDriven",
|
||||
"isActiveSteered",
|
||||
"position",
|
||||
"diameter",
|
||||
"width"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "wheel type. DRIVE, CASTER, FIXED, MECANUM",
|
||||
"enum": [
|
||||
"DRIVE",
|
||||
"CASTER",
|
||||
"FIXED",
|
||||
"MECANUM"
|
||||
]
|
||||
},
|
||||
"isActiveDriven": {
|
||||
"type": "boolean",
|
||||
"description": "True: wheel is actively driven (de: angetrieben)"
|
||||
},
|
||||
"isActiveSteered": {
|
||||
"type": "boolean",
|
||||
"description": "True: wheel is actively steered (de: aktiv gelenkt)"
|
||||
},
|
||||
"position": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"description": "[m] x-position in AGV-coordinate system",
|
||||
"unit": "m"
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"description": "y-position in AGV-coordinate system",
|
||||
"unit": "m"
|
||||
},
|
||||
"theta": {
|
||||
"type": "number",
|
||||
"description": "orientation of wheel in AGV-coordinate system Necessary for fixed wheels",
|
||||
"unit": "rad"
|
||||
}
|
||||
}
|
||||
},
|
||||
"diameter": {
|
||||
"type": "number",
|
||||
"description": "nominal diameter of wheel",
|
||||
"unit": "m"
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"description": "nominal width of wheel",
|
||||
"unit": "m"
|
||||
},
|
||||
"centerDisplacement": {
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"description": "nominal displacement of the wheel’s center to the rotation point (necessary for caster wheels). If the parameter is not defined, it is assumed to be 0"
|
||||
},
|
||||
"constraints": {
|
||||
"type": "string",
|
||||
"description": "free text: can be used by the manufacturer to define constraints"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"envelopes2d": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"set",
|
||||
"polygonPoints"
|
||||
],
|
||||
"properties": {
|
||||
"set": {
|
||||
"type": "string",
|
||||
"description": "name of the envelope curve set"
|
||||
},
|
||||
"polygonPoints": {
|
||||
"type": "array",
|
||||
"description": "envelope curve as a x/y-polygon polygon is assumed as closed and must be non-self-intersecting",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"description": "x-position of polygon-point",
|
||||
"unit": "m"
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"description": " y-position of polygon-point",
|
||||
"unit": "m"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "free text: description of envelope curve set"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"envelopes3d": {
|
||||
"type": "array",
|
||||
"description": "list of AGV-envelope curves in 3D (german: „Hüllkurven“)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"set",
|
||||
"format"
|
||||
],
|
||||
"properties": {
|
||||
"set": {
|
||||
"type": "string",
|
||||
"description": "name of the envelope curve set"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "format of data e.g. DXF"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "3D-envelope curve data, format specified in ‚format‘"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "protocol and url-definition for downloading the 3D-envelope curve data e.g. ftp://xxx.yyy.com/ac4dgvhoif5tghji"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "free text: description of envelope curve set"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"loadSpecification": {
|
||||
"type": "object",
|
||||
"description": "Abstract specification of load capabilities",
|
||||
"properties": {
|
||||
"loadPositions": {
|
||||
"type": "array",
|
||||
"description": "list of load positions / load handling devices. This lists contains the valid values for the parameter “state.loads[].loadPosition” and for the action parameter “lhd” of the actions pick and drop. If this list doesn’t exist or is empty, the AGV has no load handling device.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"loadSets": {
|
||||
"type": "array",
|
||||
"description": "list of load-sets that can be handled by the AGV",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"setName",
|
||||
"loadType"
|
||||
],
|
||||
"properties": {
|
||||
"setName": {
|
||||
"type": "string",
|
||||
"description": "Unique name of the load set, e.g. DEFAULT, SET1, ..."
|
||||
},
|
||||
"loadType": {
|
||||
"type": "string",
|
||||
"description": "type of load e.g. EPAL, XLT1200, …."
|
||||
},
|
||||
"loadPositions": {
|
||||
"type": "array",
|
||||
"description": "list of load positions btw. load handling devices, this load-set is valid for. If this parameter does not exist or is empty, this load-set is valid for all load handling devices on this AGV.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"boundingBoxReference": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"description": "bounding box reference as defined in parameter loads[] in state-message",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"description": "x-coordinate of the point of reference."
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"description": "y-coordinate of the point of reference."
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"description": "z-coordinate of the point of reference."
|
||||
},
|
||||
"theta": {
|
||||
"type": "number",
|
||||
"description": "Orientation of the loads bounding box. Important for tugger trains, etc."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loadDimensions": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"length",
|
||||
"width"
|
||||
],
|
||||
"properties": {
|
||||
"length": {
|
||||
"type": "number",
|
||||
"description": "Absolute length of the load´s bounding box."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"description": "Absolute width of the load´s bounding bo"
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"description": "Absolute height of the load´s bounding box. Optional: Set value only if known."
|
||||
}
|
||||
}
|
||||
},
|
||||
"maxWeight": {
|
||||
"type": "number",
|
||||
"description": "maximum weight of loadtype",
|
||||
"unit": "kg"
|
||||
},
|
||||
"minLoadhandlingHeight": {
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"description": "minimum allowed height for handling of this load-type and –weight. References to boundingBoxReference"
|
||||
},
|
||||
"maxLoadhandlingHeight": {
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"description": "maximum allowed height for handling of this load-type and –weight. references to boundingBoxReference"
|
||||
},
|
||||
"minLoadhandlingDepth": {
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"description": "minimum allowed depth for this load-type and –weight. references to boundingBoxReference"
|
||||
},
|
||||
"maxLoadhandlingDepth": {
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"description": "maximum allowed depth for this load-type and –weight. references to boundingBoxReference"
|
||||
},
|
||||
"minLoadhandlingTilt": {
|
||||
"type": "number",
|
||||
"unit": "rad",
|
||||
"description": "minimum allowed tilt for this load-type and –weight"
|
||||
},
|
||||
"maxLoadhandlingTilt": {
|
||||
"type": "number",
|
||||
"unit": "rad",
|
||||
"description": "maximum allowed tilt for this load-type and –weight"
|
||||
},
|
||||
"agvSpeedLimit": {
|
||||
"type": "number",
|
||||
"unit": "m/s^2",
|
||||
"description": "maximum allowed speed for this load-type and –weight"
|
||||
},
|
||||
"agvAccelerationLimit": {
|
||||
"type": "number",
|
||||
"unit": "m/s^2",
|
||||
"description": "maximum allowed acceleration for this load-type and –weight"
|
||||
},
|
||||
"agvDecelerationLimit": {
|
||||
"type": "number",
|
||||
"unit": "m/s^2",
|
||||
"description": "maximum allowed deceleration for this load-type and –weight"
|
||||
},
|
||||
"pickTime": {
|
||||
"type": "number",
|
||||
"unit": "s",
|
||||
"description": "approx. time for picking up the load"
|
||||
},
|
||||
"dropTime": {
|
||||
"type": "number",
|
||||
"unit": "s",
|
||||
"description": "approx. time for dropping the load"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "free text description of the load handling set"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicleConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"versions": {
|
||||
"type": "array",
|
||||
"description": "Array containing various hardware and software versions running on the vehicle.",
|
||||
"items": {
|
||||
"title": "version",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key of the version.",
|
||||
"examples": [
|
||||
"softwareVersion",
|
||||
"cameraVersion",
|
||||
"plcSoftChecksum"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value of the action parameter.",
|
||||
"examples": [
|
||||
"v1.03.2",
|
||||
"0620NL51805A0",
|
||||
"0x4297F30C"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dnsServers": {
|
||||
"type": "array",
|
||||
"description": "List of DNS servers used by the vehicle.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"localIpAddress": {
|
||||
"type": "string",
|
||||
"description": "A priori assigned IP address of the vehicle used to communicate with the MQTT broker. Note that this IP address should not be modified/changed during operations."
|
||||
},
|
||||
"ntpServers": {
|
||||
"type": "array",
|
||||
"description": "List of NTP servers used by the vehicle.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"netmask": {
|
||||
"type": "string",
|
||||
"description": "Network subnet mask."
|
||||
},
|
||||
"defaultGateway": {
|
||||
"type": "string",
|
||||
"description": "Default gateway used by the vehicle."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "instantActions",
|
||||
"description": "JSON Schema for publishing instantActions that the AGV is to execute as soon as they arrive.",
|
||||
"required": [
|
||||
"headerId",
|
||||
"timestamp",
|
||||
"version",
|
||||
"manufacturer",
|
||||
"serialNumber",
|
||||
"actions"
|
||||
],
|
||||
"subtopic": "/instantActions",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"headerId": {
|
||||
"title": "headerId",
|
||||
"type": "integer",
|
||||
"description": "headerId of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message."
|
||||
},
|
||||
"timestamp": {
|
||||
"title": "timestamp",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ffZ).",
|
||||
"examples": [
|
||||
"1991-03-11T11:40:03.12Z"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"title": "Version",
|
||||
"type": "string",
|
||||
"description": "Version of the protocol [Major].[Minor].[Patch]",
|
||||
"examples": [
|
||||
"1.3.2"
|
||||
]
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"description": "Manufacturer of the AGV"
|
||||
},
|
||||
"serialNumber": {
|
||||
"type": "string",
|
||||
"description": "Serial number of the AGV."
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Describes an action that the AGV can perform.",
|
||||
"required": [
|
||||
"actionId",
|
||||
"actionType",
|
||||
"blockingType"
|
||||
],
|
||||
"properties": {
|
||||
"actionType": {
|
||||
"type": "string",
|
||||
"description": "Name of action as described in the first column of \"Actions and Parameters\". Identifies the function of the action."
|
||||
},
|
||||
"actionId": {
|
||||
"type": "string",
|
||||
"description": "Unique ID to identify the action and map them to the actionState in the state. Suggestion: Use UUIDs."
|
||||
},
|
||||
"actionDescription": {
|
||||
"type": "string",
|
||||
"description": "Additional information on the action."
|
||||
},
|
||||
"blockingType": {
|
||||
"type": "string",
|
||||
"description": "Regulates if the action is allowed to be executed during movement and/or parallel to other actions.\nnone: action can happen in parallel with others, including movement.\nsoft: action can happen simultaneously with others, but not while moving.\nhard: no other actions can be performed while this action is running.",
|
||||
"enum": [
|
||||
"NONE",
|
||||
"SOFT",
|
||||
"HARD"
|
||||
]
|
||||
},
|
||||
"actionParameters": {
|
||||
"type": "array",
|
||||
"description": "Array of actionParameter-objects for the indicated action e. g. deviceId, loadId, external Triggers.",
|
||||
"items": {
|
||||
"title": "actionParameter",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key of the action parameter.",
|
||||
"examples": [
|
||||
"duration",
|
||||
"direction",
|
||||
"signal"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"array",
|
||||
"boolean",
|
||||
"number",
|
||||
"string",
|
||||
"object"
|
||||
],
|
||||
"description": "The value of the action parameter",
|
||||
"examples": [
|
||||
103.2,
|
||||
"left",
|
||||
true,
|
||||
[
|
||||
"arrays",
|
||||
"are",
|
||||
"also",
|
||||
"valid"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Order Message",
|
||||
"description": "The message schema to communicate orders from master control to the AGV.",
|
||||
"subtopic": "/order",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"headerId",
|
||||
"timestamp",
|
||||
"version",
|
||||
"manufacturer",
|
||||
"serialNumber",
|
||||
"orderId",
|
||||
"orderUpdateId",
|
||||
"nodes",
|
||||
"edges"
|
||||
],
|
||||
"properties": {
|
||||
"headerId": {
|
||||
"type": "integer",
|
||||
"description": "headerId of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message."
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ffZ).",
|
||||
"examples": [
|
||||
"1991-03-11T11:40:03.12Z"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Version of the protocol [Major].[Minor].[Patch]",
|
||||
"examples": [
|
||||
"1.3.2"
|
||||
]
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"description": "Manufacturer of the AGV"
|
||||
},
|
||||
"serialNumber": {
|
||||
"type": "string",
|
||||
"description": "Serial number of the AGV."
|
||||
},
|
||||
"orderId": {
|
||||
"description": "Order Identification. This is to be used to identify multiple order messages that belong to the same order.",
|
||||
"type": "string"
|
||||
},
|
||||
"orderUpdateId": {
|
||||
"description": "orderUpdate identification. Is unique per orderId. If an order update is rejected, this field is to be passed in the rejection message.",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"zoneSetId": {
|
||||
"description": "Unique identifier of the zone set that the AGV has to use for navigation or that was used by MC for planning.\nOptional: Some MC systems do not use zones. Some AGVs do not understand zones. Do not add to message if no zones are used.",
|
||||
"type": "string"
|
||||
},
|
||||
"nodes": {
|
||||
"description": "Array of nodes objects to be traversed for fulfilling the order. One node is enough for a valid order. Leave edge list empty for that case.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "node",
|
||||
"required": [
|
||||
"nodeId",
|
||||
"sequenceId",
|
||||
"released",
|
||||
"actions"
|
||||
],
|
||||
"properties": {
|
||||
"nodeId": {
|
||||
"type": "string",
|
||||
"description": "Unique node identification",
|
||||
"examples": [
|
||||
"pumpenhaus_1",
|
||||
"MONTAGE"
|
||||
]
|
||||
},
|
||||
"sequenceId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number to track the sequence of nodes and edges in an order and to simplify order updates.\nThe main purpose is to distinguish between a node which is passed more than once within one orderId. The variable sequenceId runs across all nodes and edges of the same order and is reset when a new orderId is issued."
|
||||
},
|
||||
"nodeDescription": {
|
||||
"type": "string",
|
||||
"description": "Additional information on the node."
|
||||
},
|
||||
"released": {
|
||||
"type": "boolean",
|
||||
"description": "True indicates that the node is part of the base. False indicates that the node is part of the horizon."
|
||||
},
|
||||
"nodePosition": {
|
||||
"description": "Defines the position on a map in world coordinates. Each floor has its own map. All maps must use the same project specific global origin. \nOptional for vehicle-types that do not require the node position (e.g., line-guided vehicles).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"mapId"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"description": "X-position on the map in reference to the map coordinate system. Precision is up to the specific implementation."
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"description":"Y-position on the map in reference to the map coordinate system. Precision is up to the specific implementation."
|
||||
},
|
||||
"theta": {
|
||||
"type": "number",
|
||||
"description": "Absolute orientation of the AGV on the node. \nOptional: vehicle can plan the path by itself.\nIf defined, the AGV has to assume the theta angle on this node. If previous edge disallows rotation, the AGV must rotate on the node. If following edge has a differing orientation defined but disallows rotation, the AGV is to rotate on the node to the edges desired rotation before entering the edge.",
|
||||
"minimum": -3.14159265359,
|
||||
"maximum": 3.14159265359
|
||||
},
|
||||
"allowedDeviationXY": {
|
||||
"type": "number",
|
||||
"description": "Indicates how exact an AGV has to drive over a node in order for it to count as traversed.\nIf = 0: no deviation is allowed (no deviation means within the normal tolerance of the AGV manufacturer).\nIf > 0: allowed deviation-radius in meters. If the AGV passes a node within the deviation-radius, the node is considered to have been traversed.",
|
||||
"minimum": 0
|
||||
},
|
||||
"allowedDeviationTheta": {
|
||||
"type": "number",
|
||||
"minimum": 0.0,
|
||||
"maximum": 3.141592654,
|
||||
"description": "Indicates how big the deviation of theta angle can be. \nThe lowest acceptable angle is theta - allowedDeviationTheta and the highest acceptable angle is theta + allowedDeviationTheta."
|
||||
},
|
||||
"mapId": {
|
||||
"description": "Unique identification of the map in which the position is referenced.\nEach map has the same origin of coordinates. When an AGV uses an elevator, e.g., leading from a departure floor to a target floor, it will disappear off the map of the departure floor and spawn in the related lift node on the map of the target floor.",
|
||||
"type": "string"
|
||||
},
|
||||
"mapDescription": {
|
||||
"description": "Additional information on the map.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"description": "Array of actions to be executed on a node. Empty array, if no actions required.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/action"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edges": {
|
||||
"type": "array",
|
||||
"description": "Directional connection between two nodes. Array of edge objects to be traversed for fulfilling the order. One node is enough for a valid order. Leave edge list empty for that case.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "edge",
|
||||
"required": [
|
||||
"edgeId",
|
||||
"sequenceId",
|
||||
"released",
|
||||
"startNodeId",
|
||||
"endNodeId",
|
||||
"actions"
|
||||
],
|
||||
"properties": {
|
||||
"edgeId": {
|
||||
"type": "string",
|
||||
"description": "Unique edge identification"
|
||||
},
|
||||
"sequenceId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number to track the sequence of nodes and edges in an order and to simplify order updates. The variable sequenceId runs across all nodes and edges of the same order and is reset when a new orderId is issued."
|
||||
},
|
||||
"edgeDescription": {
|
||||
"type": "string",
|
||||
"description": "Additional information on the edge."
|
||||
},
|
||||
"released": {
|
||||
"type": "boolean",
|
||||
"description": "True indicates that the edge is part of the base. False indicates that the edge is part of the horizon."
|
||||
},
|
||||
"startNodeId": {
|
||||
"type": "string",
|
||||
"description": "The nodeId of the start node."
|
||||
},
|
||||
"endNodeId": {
|
||||
"type": "string",
|
||||
"description": "The nodeId of the end node."
|
||||
},
|
||||
"maxSpeed": {
|
||||
"type": "number",
|
||||
"description": "Permitted maximum speed on the edge in m/s. Speed is defined by the fastest measurement of the vehicle."
|
||||
},
|
||||
"maxHeight": {
|
||||
"type": "number",
|
||||
"description": "Permitted maximum height of the vehicle, including the load, on edge in meters."
|
||||
},
|
||||
"minHeight": {
|
||||
"type": "number",
|
||||
"description": "Permitted minimal height of the load handling device on the edge in meters"
|
||||
},
|
||||
"orientation": {
|
||||
"type": "number",
|
||||
"description": "Orientation of the AGV on the edge. The value orientationType defines if it has to be interpreted relative to the global project specific map coordinate system or tangential to the edge. In case of interpreted tangential to the edge 0.0 = forwards and PI = backwards. Example: orientation Pi/2 rad will lead to a rotation of 90 degrees. \nIf AGV starts in different orientation, rotate the vehicle on the edge to the desired orientation if rotationAllowed is set to True. If rotationAllowed is False, rotate before entering the edge. If that is not possible, reject the order. \nIf no trajectory is defined, apply the rotation to the direct path between the two connecting nodes of the edge. If a trajectory is defined for the edge, apply the orientation to the trajectory.",
|
||||
"minimum": -3.14159265359,
|
||||
"maximum": 3.14159265359
|
||||
},
|
||||
"orientationType":{
|
||||
"type": "string",
|
||||
"description": "Enum {GLOBAL, TANGENTIAL}: \n\"GLOBAL\"- relative to the global project specific map coordinate system; \n\"TANGENTIAL\"- tangential to the edge. \nIf not defined, the default value is \"TANGENTIAL\"."
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"description": "Sets direction at junctions for line-guided or wire-guided vehicles, to be defined initially (vehicle-individual)."
|
||||
},
|
||||
"rotationAllowed": {
|
||||
"type": "boolean",
|
||||
"description": "True: rotation is allowed on the edge. False: rotation is not allowed on the edge. \nOptional: No limit, if not set."
|
||||
},
|
||||
"maxRotationSpeed": {
|
||||
"type": "number",
|
||||
"description": "Maximum rotation speed in rad/s. \nOptional: No limit, if not set."
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"description": "Distance of the path from startNode to endNode in meters. \nOptional: This value is used by line-guided AGVs to decrease their speed before reaching a stop position."
|
||||
},
|
||||
"trajectory": {
|
||||
"type": "object",
|
||||
"description": "Trajectory JSON-object for this edge as a NURBS. Defines the curve, on which the AGV should move between startNode and endNode. \nOptional: Can be omitted, if AGV cannot process trajectories or if AGV plans its own trajectory.",
|
||||
"required": [
|
||||
"degree",
|
||||
"knotVector",
|
||||
"controlPoints"
|
||||
],
|
||||
"properties": {
|
||||
"degree": {
|
||||
"type": "integer",
|
||||
"description": "Defines the number of control points that influence any given point on the curve. Increasing the degree increases continuity. If not defined, the default value is 1.",
|
||||
"minimum": 1
|
||||
},
|
||||
"knotVector": {
|
||||
"type": "array",
|
||||
"description": "Sequence of parameter values that determines where and how the control points affect the NURBS curve. knotVector has size of number of control points + degree + 1.",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"maximum": 1,
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"controlPoints": {
|
||||
"type": "array",
|
||||
"description": "List of JSON controlPoint objects defining the control points of the NURBS, which includes the beginning and end point.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "controlPoint",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"description": "X coordinate described in the world coordinate system."
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"description": "Y coordinate described in the world coordinate system."
|
||||
},
|
||||
"weight": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"description": "The weight, with which this control point pulls on the curve. When not defined, the default will be 1.0."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"corridor": {
|
||||
"description": "Definition of boundaries in which a vehicle can deviate from its trajectory, e. g. to avoid obstacles.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"leftWidth",
|
||||
"rightWidth"
|
||||
],
|
||||
"properties": {
|
||||
"leftWidth": {
|
||||
"type": "number",
|
||||
"description": "Defines the width of the corridor in meters to the left related to the trajectory of the vehicle.",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"rightWidth": {
|
||||
"type": "number",
|
||||
"description":"Defines the width of the corridor in meters to the right related to the trajectory of the vehicle.",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"corridorRefPoint": {
|
||||
"type": "string",
|
||||
"description": "Defines whether the boundaries are valid for the kinematic center or the contour of the vehicle.",
|
||||
"enum": [
|
||||
"KINEMATICCENTER",
|
||||
"CONTOUR"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"description": "Array of action objects with detailed information.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/action"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"action": {
|
||||
"type": "object",
|
||||
"description": "Describes an action that the AGV can perform.",
|
||||
"required": [
|
||||
"actionId",
|
||||
"actionType",
|
||||
"blockingType"
|
||||
],
|
||||
"properties": {
|
||||
"actionType": {
|
||||
"type": "string",
|
||||
"description": "Name of action as described in the first column of \"Actions and Parameters\". Identifies the function of the action."
|
||||
},
|
||||
"actionId": {
|
||||
"type": "string",
|
||||
"description": "Unique ID to identify the action and map them to the actionState in the state. Suggestion: Use UUIDs."
|
||||
},
|
||||
"actionDescription": {
|
||||
"type": "string",
|
||||
"description": "Additional information on the action."
|
||||
},
|
||||
"blockingType": {
|
||||
"type": "string",
|
||||
"description": "Regulates if the action is allowed to be executed during movement and/or parallel to other actions.\nnone: action can happen in parallel with others, including movement.\nsoft: action can happen simultaneously with others, but not while moving.\nhard: no other actions can be performed while this action is running.",
|
||||
"enum": [
|
||||
"NONE",
|
||||
"SOFT",
|
||||
"HARD"
|
||||
]
|
||||
},
|
||||
"actionParameters": {
|
||||
"type": "array",
|
||||
"description": "Array of actionParameter-objects for the indicated action e. g. deviceId, loadId, external Triggers.",
|
||||
"items": {
|
||||
"title": "actionParameter",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key of the action parameter.",
|
||||
"examples": [
|
||||
"duration",
|
||||
"direction",
|
||||
"signal"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"array",
|
||||
"boolean",
|
||||
"number",
|
||||
"string",
|
||||
"object"
|
||||
],
|
||||
"description": "The value of the action parameter",
|
||||
"examples": [
|
||||
103.2,
|
||||
"left",
|
||||
true,
|
||||
[
|
||||
"arrays",
|
||||
"are",
|
||||
"also",
|
||||
"valid"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "state",
|
||||
"description": "all encompassing state of the AGV.",
|
||||
"subtopic": "/state",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"headerId",
|
||||
"timestamp",
|
||||
"version",
|
||||
"manufacturer",
|
||||
"serialNumber",
|
||||
"orderId",
|
||||
"orderUpdateId",
|
||||
"lastNodeId",
|
||||
"lastNodeSequenceId",
|
||||
"nodeStates",
|
||||
"edgeStates",
|
||||
"driving",
|
||||
"actionStates",
|
||||
"batteryState",
|
||||
"operatingMode",
|
||||
"errors",
|
||||
"safetyState"
|
||||
],
|
||||
"properties": {
|
||||
"headerId": {
|
||||
"type": "integer",
|
||||
"description": "headerId of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message."
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ffZ).",
|
||||
"examples": [
|
||||
"1991-03-11T11:40:03.12Z"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Version of the protocol [Major].[Minor].[Patch]",
|
||||
"examples": [
|
||||
"1.3.2"
|
||||
]
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"description": "Manufacturer of the AGV"
|
||||
},
|
||||
"serialNumber": {
|
||||
"type": "string",
|
||||
"description": "Serial number of the AGV."
|
||||
},
|
||||
"maps":{
|
||||
"type": "array",
|
||||
"description": "Array of map-objects that are currently stored on the vehicle.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "map",
|
||||
"required": [
|
||||
"mapId",
|
||||
"mapVersion",
|
||||
"mapStatus"
|
||||
],
|
||||
"properties": {
|
||||
"mapId": {
|
||||
"type": "string",
|
||||
"description": "ID of the map describing a defined area of the vehicle's workspace."
|
||||
},
|
||||
"mapVersion": {
|
||||
"type": "string",
|
||||
"description": "Version of the map."
|
||||
},
|
||||
"mapDescription": {
|
||||
"type": "string",
|
||||
"description": "Additional information on the map."
|
||||
},
|
||||
"mapStatus": {
|
||||
"type": "string",
|
||||
"description": "Information on the status of the map indicating, if a map version is currently used on the vehicle. ENABLED: Indicates this map is currently active / used on the AGV. At most one map with the same mapId can have its status set to ENABLED.<br>DISABLED: Indicates this map version is currently not enabled on the AGV and thus could be enabled or deleted by request.",
|
||||
"enum": [
|
||||
"ENABLED",
|
||||
"DISABLED"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"orderId": {
|
||||
"type": "string",
|
||||
"description": "Unique order identification of the current order or the previous finished order. The orderId is kept until a new order is received. Empty string (\"\") if no previous orderId is available. "
|
||||
},
|
||||
"orderUpdateId": {
|
||||
"type": "integer",
|
||||
"description": "Order Update Identification to identify that an order update has been accepted by the AGV. \"0\" if no previous orderUpdateId is available."
|
||||
},
|
||||
"zoneSetId": {
|
||||
"type": "string",
|
||||
"description": "Unique ID of the zone set that the AGV currently uses for path planning. Must be the same as the one used in the order, otherwise the AGV is to reject the order.\nOptional: If the AGV does not use zones, this field can be omitted."
|
||||
},
|
||||
"lastNodeId": {
|
||||
"type": "string",
|
||||
"description": "nodeID of last reached node or, if AGV is currently on a node, current node (e.g., \"node7\"). Empty string (\"\") if no lastNodeId is available."
|
||||
},
|
||||
"lastNodeSequenceId": {
|
||||
"type": "integer",
|
||||
"description": "sequenceId of the last reached node or, if the AGV is currently on a node, sequenceId of current node. \"0\" if no lastNodeSequenceId is available. "
|
||||
},
|
||||
"driving": {
|
||||
"type": "boolean",
|
||||
"description": "True: indicates that the AGV is driving and/or rotating. Other movements of the AGV (e.g., lift movements) are not included here.\nFalse: indicates that the AGV is neither driving nor rotating "
|
||||
},
|
||||
"paused": {
|
||||
"type": "boolean",
|
||||
"description": "True: AGV is currently in a paused state, either because of the push of a physical button on the AGV or because of an instantAction. The AGV can resume the order.\nFalse: The AGV is currently not in a paused state."
|
||||
},
|
||||
"newBaseRequest": {
|
||||
"type": "boolean",
|
||||
"description": "True: AGV is almost at the end of the base and will reduce speed if no new base is transmitted. Trigger for master control to send new base\nFalse: no base update required."
|
||||
},
|
||||
"distanceSinceLastNode": {
|
||||
"type": "number",
|
||||
"description": "Used by line guided vehicles to indicate the distance it has been driving past the \"lastNodeId\".\nDistance is in meters."
|
||||
},
|
||||
"operatingMode": {
|
||||
"type": "string",
|
||||
"description": "Current operating mode of the AGV.",
|
||||
"enum": [
|
||||
"AUTOMATIC",
|
||||
"SEMIAUTOMATIC",
|
||||
"MANUAL",
|
||||
"SERVICE",
|
||||
"TEACHIN"
|
||||
]
|
||||
},
|
||||
"nodeStates": {
|
||||
"type": "array",
|
||||
"description": "Array of nodeState-Objects, that need to be traversed for fulfilling the order. Empty list if idle.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "nodeState",
|
||||
"required": [
|
||||
"nodeId",
|
||||
"sequenceId",
|
||||
"released"
|
||||
],
|
||||
"properties": {
|
||||
"nodeId": {
|
||||
"type": "string",
|
||||
"description": "Unique node identification"
|
||||
},
|
||||
"sequenceId": {
|
||||
"type": "integer",
|
||||
"description": "sequenceId to discern multiple nodes with same nodeId."
|
||||
},
|
||||
"nodeDescription": {
|
||||
"type": "string",
|
||||
"description": "Additional information on the node."
|
||||
},
|
||||
"released": {
|
||||
"type": "boolean",
|
||||
"description": "True: indicates that the node is part of the base. False: indicates that the node is part of the horizon."
|
||||
},
|
||||
"nodePosition": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"mapId"
|
||||
],
|
||||
"description": "Node position. The object is defined in chapter 5.4 Topic: Order (from master control to AGV).\nOptional:Master control has this information. Can be sent additionally, e.g., for debugging purposes. ",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
},
|
||||
"theta": {
|
||||
"type": "number"
|
||||
},
|
||||
"mapId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edgeStates": {
|
||||
"type": "array",
|
||||
"description": "Array of edgeState-Objects, that need to be traversed for fulfilling the order, empty list if idle.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"edgeId",
|
||||
"sequenceId",
|
||||
"released"
|
||||
],
|
||||
"properties": {
|
||||
"edgeId": {
|
||||
"type": "string",
|
||||
"description": "Unique edge identification"
|
||||
},
|
||||
"sequenceId": {
|
||||
"type": "integer",
|
||||
"description": "sequenceId of the edge."
|
||||
},
|
||||
"edgeDescription": {
|
||||
"type": "string",
|
||||
"description": "Additional information on the edge."
|
||||
},
|
||||
"released": {
|
||||
"type": "boolean",
|
||||
"description": "True indicates that the edge is part of the base. False indicates that the edge is part of the horizon."
|
||||
},
|
||||
"trajectory": {
|
||||
"type": "object",
|
||||
"description": "The trajectory is to be communicated as a NURBS and is defined in chapter 6.7 Implementation of the Order message.\nTrajectory segments reach from the point, where the AGV starts to enter the edge to the point where it reports that the next node was traversed. ",
|
||||
"required": [
|
||||
"degree",
|
||||
"knotVector",
|
||||
"controlPoints"
|
||||
],
|
||||
"properties": {
|
||||
"degree": {
|
||||
"type": "integer",
|
||||
"description": "Defines the number of control points that influence any given point on the curve. Increasing the degree increases continuity. If not defined, the default value is 1."
|
||||
},
|
||||
"knotVector": {
|
||||
"type": "array",
|
||||
"description": "Sequence of parameter values that determine where and how the control points affect the NURBS curve. knotVector has size of number of control points + degree + 1",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"maximum": 1.0,
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"controlPoints": {
|
||||
"type": "array",
|
||||
"description": "List of JSON controlPoint objects defining the control points of the NURBS, which includes the beginning and end point.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
},
|
||||
"weight": {
|
||||
"type": "number",
|
||||
"description": "The weight, with which this control point pulls on the curve.\nWhen not defined, the default will be 1.0."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"agvPosition": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"theta",
|
||||
"mapId",
|
||||
"positionInitialized"
|
||||
],
|
||||
"description": "Defines the position on a map in world coordinates. Each floor has its own map.",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
},
|
||||
"theta": {
|
||||
"type": "number"
|
||||
},
|
||||
"mapId": {
|
||||
"type": "string"
|
||||
},
|
||||
"mapDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"positionInitialized": {
|
||||
"type": "boolean",
|
||||
"description": "True: position is initialized. False: position is not initizalized."
|
||||
},
|
||||
"localizationScore": {
|
||||
"type": "number",
|
||||
"description": "Describes the quality of the localization and therefore, can be used, e.g., by SLAM-AGV to describe how accurate the current position information is.\n0.0: position unknown\n1.0: position known\nOptional for vehicles that cannot estimate their localization score.\nOnly for logging and visualization purposes",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
},
|
||||
"deviationRange": {
|
||||
"type": "number",
|
||||
"description": "Value for position deviation range in meters. Optional for vehicles that cannot estimate their deviation, e.g., grid-based localization. Only for logging and visualization purposes."
|
||||
}
|
||||
}
|
||||
},
|
||||
"velocity": {
|
||||
"type": "object",
|
||||
"description": "The AGVs velocity in vehicle coordinates",
|
||||
"properties": {
|
||||
"vx": {
|
||||
"type": "number",
|
||||
"description":"The AVGs velocity in its x direction"
|
||||
},
|
||||
"vy": {
|
||||
"type": "number",
|
||||
"description":"The AVGs velocity in its y direction"
|
||||
},
|
||||
"omega": {
|
||||
"type": "number",
|
||||
"description":"The AVGs turning speed around its z axis."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loads": {
|
||||
"type": "array",
|
||||
"description": "Loads, that are currently handled by the AGV. Optional: If AGV cannot determine load state, leave the array out of the state. If the AGV can determine the load state, but the array is empty, the AGV is considered unloaded.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"description": "Load object that describes the load if the AGV has information about it.",
|
||||
"title": "load",
|
||||
"properties": {
|
||||
"loadId": {
|
||||
"type": "string",
|
||||
"description": "Unique identification number of the load (e.g., barcode or RFID). Empty field, if the AGV can identify the load, but did not identify the load yet. Optional, if the AGV cannot identify the load."
|
||||
},
|
||||
"loadType": {
|
||||
"type": "string",
|
||||
"description":"Type of load."
|
||||
},
|
||||
"loadPosition": {
|
||||
"type": "string",
|
||||
"description": "Indicates, which load handling/carrying unit of the AGV is used, e.g., in case the AGV has multiple spots/positions to carry loads. Optional for vehicles with only one loadPosition.",
|
||||
"examples":[
|
||||
"front", "back", "positionC1"
|
||||
]
|
||||
},
|
||||
"boundingBoxReference": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"description": "Point of reference for the location of the bounding box. The point of reference is always the center of the bounding box bottom surface (at height = 0) and is described in coordinates of the AGV coordinate system.",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
},
|
||||
"z": {
|
||||
"type": "number"
|
||||
},
|
||||
"theta": {
|
||||
"type": "number",
|
||||
"description":"Orientation of the loads bounding box. Important for tugger, trains, etc."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loadDimensions": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"length",
|
||||
"width"
|
||||
],
|
||||
"description": "Dimensions of the loads bounding box in meters.",
|
||||
"properties": {
|
||||
"length": {
|
||||
"type": "number",
|
||||
"description": "Absolute length of the loads bounding box in meter."
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"description": "Absolute width of the loads bounding box in meter."
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"description": "Absolute height of the loads bounding box in meter.\nOptional:\nSet value only if known."
|
||||
}
|
||||
}
|
||||
},
|
||||
"weight": {
|
||||
"type": "number",
|
||||
"description": "Absolute weight of the load measured in kg.",
|
||||
"minimum":0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"actionStates": {
|
||||
"type": "array",
|
||||
"description": "Contains a list of the current actions and the actions which are yet to be finished. This may include actions from previous nodes that are still in progress\nWhen an action is completed, an updated state message is published with actionStatus set to finished and if applicable with the corresponding resultDescription. The actionStates are kept until a new order is received.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"actionId",
|
||||
"actionStatus"
|
||||
],
|
||||
"title": "actionState",
|
||||
"properties": {
|
||||
"actionId": {
|
||||
"type": "string",
|
||||
"description": "Unique actionId",
|
||||
"examples": [
|
||||
"blink_123jdaimoim234"
|
||||
]
|
||||
},
|
||||
"actionType": {
|
||||
"type": "string",
|
||||
"description": "actionType of the action.\nOptional: Only for informational or visualization purposes. Order knows the type."
|
||||
},
|
||||
"actionDescription": {
|
||||
"type": "string",
|
||||
"description": "Additional information on the current action."
|
||||
},
|
||||
"actionStatus": {
|
||||
"type": "string",
|
||||
"description": "WAITING: waiting for the trigger (passing the mode, entering the edge) PAUSED: paused by instantAction or external trigger FAILED: action could not be performed.",
|
||||
"enum": [
|
||||
"WAITING",
|
||||
"INITIALIZING",
|
||||
"RUNNING",
|
||||
"PAUSED",
|
||||
"FINISHED",
|
||||
"FAILED"
|
||||
]
|
||||
},
|
||||
"resultDescription": {
|
||||
"type": "string",
|
||||
"description": "Description of the result, e.g., the result of a RFID-read. Errors will be transmitted in errors."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"batteryState": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"batteryCharge",
|
||||
"charging"
|
||||
],
|
||||
"description": "Contains all battery-related information.",
|
||||
"properties": {
|
||||
"batteryCharge": {
|
||||
"type": "number",
|
||||
"description": "State of Charge in %:\nIf AGV only provides values for good or bad battery levels, these will be indicated as 20% (bad) and 80% (good)."
|
||||
},
|
||||
"batteryVoltage": {
|
||||
"type": "number",
|
||||
"description": "Battery voltage"
|
||||
},
|
||||
"batteryHealth": {
|
||||
"type": "number",
|
||||
"description": "State of health in percent.",
|
||||
"minimum":0,
|
||||
"maximum":100
|
||||
},
|
||||
"charging": {
|
||||
"type": "boolean",
|
||||
"description": "True: charging in progress. False: AGV is currently not charging."
|
||||
},
|
||||
"reach": {
|
||||
"type": "number",
|
||||
"description": "Estimated reach with current State of Charge in meter.",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"description": "Array of error-objects. All active errors of the AGV should be in the list. An empty array indicates that the AGV has no active errors.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"errorType",
|
||||
"errorLevel"
|
||||
],
|
||||
"title": "Error",
|
||||
"properties": {
|
||||
"errorType": {
|
||||
"type": "string",
|
||||
"description": "Type/name of error."
|
||||
},
|
||||
"errorReferences": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "errorReference",
|
||||
"description": "Array of references (e.g. nodeId, edgeId, orderId, actionId, etc.) to provide more information related to the error.",
|
||||
"properties": {
|
||||
"referenceKey": {
|
||||
"type": "string",
|
||||
"description":"Specifies the type of reference used (e.g. nodeId, edgeId, orderId, actionId, etc.)."
|
||||
},
|
||||
"referenceValue": {
|
||||
"type": "string",
|
||||
"description":"The value that belongs to the reference key. For example, the id of the node where the error occurred."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"referenceKey",
|
||||
"referenceValue"
|
||||
]
|
||||
}
|
||||
},
|
||||
"errorDescription": {
|
||||
"type": "string",
|
||||
"description": "Verbose description providing details and possible causes of the error."
|
||||
},
|
||||
"errorHint": {
|
||||
"type": "string",
|
||||
"description": "Hint on how to approach or solve the reported error."
|
||||
},
|
||||
"errorLevel": {
|
||||
"type": "string",
|
||||
"description": "WARNING: AGV is ready to start (e.g., maintenance cycle expiration warning). FATAL: AGV is not in running condition, user intervention required (e.g., laser scanner is contaminated).",
|
||||
"enum": [
|
||||
"WARNING",
|
||||
"FATAL"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"information": {
|
||||
"type": "array",
|
||||
"description": "Array of info-objects. An empty array indicates, that the AGV has no information. This should only be used for visualization or debugging – it must not be used for logic in master control.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"infoType",
|
||||
"infoLevel"
|
||||
],
|
||||
"properties": {
|
||||
"infoType": {
|
||||
"type": "string",
|
||||
"description": "Type/name of information."
|
||||
},
|
||||
"infoReferences": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"referenceKey",
|
||||
"referenceValue"
|
||||
],
|
||||
"title": "infoReference",
|
||||
"description": "Array of references.",
|
||||
"properties": {
|
||||
"referenceKey": {
|
||||
"type": "string",
|
||||
"description":"References the type of reference (e.g., headerId, orderId, actionId, etc.)."
|
||||
},
|
||||
"referenceValue": {
|
||||
"type": "string",
|
||||
"description":"References the value, which belongs to the reference key."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"infoDescription": {
|
||||
"type": "string",
|
||||
"description": "Info of description."
|
||||
},
|
||||
"infoLevel": {
|
||||
"type": "string",
|
||||
"description": "DEBUG: used for debugging. INFO: used for visualization.",
|
||||
"enum": [
|
||||
"INFO",
|
||||
"DEBUG"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"safetyState": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"eStop",
|
||||
"fieldViolation"
|
||||
],
|
||||
"description": "Contains all safety-related information.",
|
||||
"properties": {
|
||||
"eStop": {
|
||||
"type": "string",
|
||||
"description": "Acknowledge-Type of eStop: AUTOACK: auto-acknowledgeable e-stop is activated, e.g., by bumper or protective field. MANUAL: e-stop hast to be acknowledged manually at the vehicle. REMOTE: facility e-stop has to be acknowledged remotely. NONE: no e-stop activated.",
|
||||
"enum": [
|
||||
"AUTOACK",
|
||||
"MANUAL",
|
||||
"REMOTE",
|
||||
"NONE"
|
||||
]
|
||||
},
|
||||
"fieldViolation": {
|
||||
"type": "boolean",
|
||||
"description": "Protective field violation. True: field is violated. False: field is not violated."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "visualization",
|
||||
"description": "AGV position and/or velocity for visualization purposes. Can be published at a higher rate if wanted. Since bandwidth may be expensive depening on the update rate for this topic, all fields are optional.",
|
||||
"subtopic": "/visualization",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"headerId": {
|
||||
"type": "integer",
|
||||
"description": "headerId of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message."
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ffZ).",
|
||||
"examples": [
|
||||
"1991-03-11T11:40:03.12Z"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Version of the protocol [Major].[Minor].[Patch]",
|
||||
"examples": [
|
||||
"1.3.2"
|
||||
]
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"description": "Manufacturer of the AGV"
|
||||
},
|
||||
"serialNumber": {
|
||||
"type": "string",
|
||||
"description": "Serial number of the AGV."
|
||||
},
|
||||
"agvPosition": {
|
||||
"type": "object",
|
||||
"title": "agvPosition",
|
||||
"description": "The AGVs position",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"theta",
|
||||
"mapId",
|
||||
"positionInitialized"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
},
|
||||
"theta": {
|
||||
"type": "number"
|
||||
},
|
||||
"mapId": {
|
||||
"type": "string"
|
||||
},
|
||||
"mapDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"positionInitialized": {
|
||||
"type": "boolean",
|
||||
"description": "True if the AGVs position is initialized, false, if position is not initizalized."
|
||||
},
|
||||
"localizationScore": {
|
||||
"type": "number",
|
||||
"description": "Localization score for SLAM based vehicles, if the AGV can communicate it.",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
},
|
||||
"deviationRange": {
|
||||
"type": "number",
|
||||
"description": "Value for position deviation range in meters. Can be used if the AGV is able to derive it."
|
||||
}
|
||||
}
|
||||
},
|
||||
"velocity": {
|
||||
"type": "object",
|
||||
"title": "velocity",
|
||||
"description": "The AGVs velocity in vehicle coordinates",
|
||||
"properties": {
|
||||
"vx": {
|
||||
"type": "number"
|
||||
},
|
||||
"vy": {
|
||||
"type": "number"
|
||||
},
|
||||
"omega": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using RobotNet.VDA5050.Type;
|
||||
|
||||
namespace RobotNet10.FleetManager.Events.Events;
|
||||
|
||||
public class ConnectionStateChangedEvent : EventArgs
|
||||
{
|
||||
public string RobotId { get; set; } = string.Empty;
|
||||
public ConnectionState ConnectionState { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using RobotNet.VDA5050.Factsheet;
|
||||
|
||||
namespace RobotNet10.FleetManager.Events.Events;
|
||||
|
||||
public class FactsheetMessageReceivedEvent : EventArgs
|
||||
{
|
||||
public string RobotId { get; set; } = string.Empty;
|
||||
public FactSheetMsg FactsheetMessage { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace RobotNet10.FleetManager.Events.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a Robot's ModelId is changed
|
||||
/// </summary>
|
||||
public class RobotModelIdChangedEvent : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Robot ID whose model was changed
|
||||
/// </summary>
|
||||
public string RobotId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Previous ModelId
|
||||
/// </summary>
|
||||
public Guid? PreviousModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// New ModelId
|
||||
/// </summary>
|
||||
public Guid? NewModelId { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace RobotNet10.FleetManager.Events.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a RobotModel is updated
|
||||
/// </summary>
|
||||
public class RobotModelUpdatedEvent : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// RobotModel ID that was updated
|
||||
/// </summary>
|
||||
public Guid ModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of robot IDs that use this model (for cache invalidation)
|
||||
/// </summary>
|
||||
public List<string> AffectedRobotIds { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using RobotNet.VDA5050.State;
|
||||
|
||||
namespace RobotNet10.FleetManager.Events.Events;
|
||||
|
||||
public class StateMessageReceivedEvent : EventArgs
|
||||
{
|
||||
public string RobotId { get; set; } = string.Empty;
|
||||
public StateMsg StateMessage { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using RobotNet.VDA5050.Visualization;
|
||||
|
||||
namespace RobotNet10.FleetManager.Events.Events;
|
||||
|
||||
public class VisualizationMessageReceivedEvent : EventArgs
|
||||
{
|
||||
public string RobotId { get; set; } = string.Empty;
|
||||
public VisualizationMsg VisualizationMessage { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using RobotNet.VDA5050.Factsheet;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Type;
|
||||
using RobotNet.VDA5050.Visualization;
|
||||
using RobotNet10.FleetManager.Events.Events;
|
||||
|
||||
namespace RobotNet10.FleetManager.Events;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory event bus for VDA5050 robot messages and domain events
|
||||
/// </summary>
|
||||
public interface IRobotEventBus
|
||||
{
|
||||
// VDA5050 Events
|
||||
event EventHandler<StateMessageReceivedEvent>? StateMessageReceived;
|
||||
event EventHandler<ConnectionStateChangedEvent>? ConnectionStateChanged;
|
||||
event EventHandler<VisualizationMessageReceivedEvent>? VisualizationMessageReceived;
|
||||
event EventHandler<FactsheetMessageReceivedEvent>? FactsheetMessageReceived;
|
||||
|
||||
// Domain Events
|
||||
event EventHandler<RobotModelUpdatedEvent>? RobotModelUpdated;
|
||||
event EventHandler<RobotModelIdChangedEvent>? RobotModelIdChanged;
|
||||
|
||||
// VDA5050 Event Publishers
|
||||
void PublishStateMessageReceived(string robotId, StateMsg stateMsg);
|
||||
void PublishConnectionStateChanged(string robotId, ConnectionState connectionState);
|
||||
void PublishVisualizationMessageReceived(string robotId, VisualizationMsg visualizationMsg);
|
||||
void PublishFactsheetMessageReceived(string robotId, FactSheetMsg factsheetMsg);
|
||||
|
||||
// Domain Event Publishers
|
||||
void PublishRobotModelUpdated(RobotModelUpdatedEvent e);
|
||||
void PublishRobotModelIdChanged(RobotModelIdChangedEvent e);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using RobotNet.VDA5050.Factsheet;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Type;
|
||||
using RobotNet.VDA5050.Visualization;
|
||||
using RobotNet10.FleetManager.Events.Events;
|
||||
|
||||
namespace RobotNet10.FleetManager.Events;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory event bus implementation for VDA5050 robot messages
|
||||
/// </summary>
|
||||
public class RobotEventBus : IRobotEventBus
|
||||
{
|
||||
public event EventHandler<StateMessageReceivedEvent>? StateMessageReceived;
|
||||
public event EventHandler<ConnectionStateChangedEvent>? ConnectionStateChanged;
|
||||
public event EventHandler<VisualizationMessageReceivedEvent>? VisualizationMessageReceived;
|
||||
public event EventHandler<FactsheetMessageReceivedEvent>? FactsheetMessageReceived;
|
||||
|
||||
public void PublishStateMessageReceived(string robotId, StateMsg stateMsg)
|
||||
{
|
||||
StateMessageReceived?.Invoke(this, new StateMessageReceivedEvent { RobotId = robotId, StateMessage = stateMsg });
|
||||
}
|
||||
|
||||
public void PublishConnectionStateChanged(string robotId, ConnectionState connectionState)
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEvent { RobotId = robotId, ConnectionState = connectionState });
|
||||
}
|
||||
|
||||
public void PublishVisualizationMessageReceived(string robotId, VisualizationMsg visualizationMsg)
|
||||
{
|
||||
VisualizationMessageReceived?.Invoke(this, new VisualizationMessageReceivedEvent { RobotId = robotId, VisualizationMessage = visualizationMsg });
|
||||
}
|
||||
|
||||
public void PublishFactsheetMessageReceived(string robotId, FactSheetMsg factsheetMsg)
|
||||
{
|
||||
FactsheetMessageReceived?.Invoke(this, new FactsheetMessageReceivedEvent { RobotId = robotId, FactsheetMessage = factsheetMsg });
|
||||
}
|
||||
|
||||
// Domain Events
|
||||
public event EventHandler<RobotModelUpdatedEvent>? RobotModelUpdated;
|
||||
public event EventHandler<RobotModelIdChangedEvent>? RobotModelIdChanged;
|
||||
|
||||
public void PublishRobotModelUpdated(RobotModelUpdatedEvent e)
|
||||
{
|
||||
RobotModelUpdated?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public void PublishRobotModelIdChanged(RobotModelIdChangedEvent e)
|
||||
{
|
||||
RobotModelIdChanged?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace RobotNet10.FleetManager.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub for real-time robot state and visualization updates.
|
||||
/// Manages client subscriptions to robot groups for receiving VDA5050 messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Clients subscribe to specific robots using SubscribeToRobot method.
|
||||
/// Messages are broadcast to groups named "robot:{robotId}".
|
||||
/// </remarks>
|
||||
public class RobotStateHub(
|
||||
Services.Logger<RobotStateHub> logger,
|
||||
RobotStateHubContext hubContext) : Hub
|
||||
{
|
||||
private readonly Services.Logger<RobotStateHub> _logger = logger;
|
||||
private readonly RobotStateHubContext _hubContext = hubContext;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to receive updates for a specific robot
|
||||
/// </summary>
|
||||
/// <param name="robotId">Robot identifier (serialNumber)</param>
|
||||
public async Task SubscribeToRobot(string robotId)
|
||||
{
|
||||
var groupName = GetGroupName(robotId);
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from updates for a specific robot
|
||||
/// </summary>
|
||||
/// <param name="robotId">Robot identifier (serialNumber)</param>
|
||||
public async Task UnsubscribeFromRobot(string robotId)
|
||||
{
|
||||
var groupName = GetGroupName(robotId);
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
|
||||
}
|
||||
|
||||
private static string GetGroupName(string robotId)
|
||||
{
|
||||
return $"robot:{robotId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to receive monitor updates for a specific levelId
|
||||
/// Each connection can only subscribe to one levelId at a time.
|
||||
/// Maximum 5 connections per levelId (FIFO).
|
||||
/// </summary>
|
||||
/// <param name="levelId">Level identifier (LayoutLevel.Id)</param>
|
||||
public async Task SubscribeToLevelForMonitor(Guid levelId)
|
||||
{
|
||||
var evictedConnectionId = _hubContext.SubscribeToLevel(Context.ConnectionId, levelId);
|
||||
|
||||
// If a connection was evicted, notify it
|
||||
if (evictedConnectionId != null)
|
||||
{
|
||||
_logger.Info($"Connection {evictedConnectionId} evicted from level {levelId} (max 5 connections reached)");
|
||||
await Clients.Client(evictedConnectionId).SendAsync("OnMonitorDeactivated");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from monitor updates for the current level
|
||||
/// </summary>
|
||||
public async Task UnsubscribeFromLevelForMonitor()
|
||||
{
|
||||
var removed = _hubContext.UnsubscribeFromLevel(Context.ConnectionId);
|
||||
if (removed)
|
||||
{
|
||||
_logger.Info($"Client {Context.ConnectionId} unsubscribed from level for monitor");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
// Remove connection from subscription manager
|
||||
_hubContext.RemoveConnection(Context.ConnectionId);
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
_logger.Warning($"Client {Context.ConnectionId} disconnected with error: {exception.Message}");
|
||||
}
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,680 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.VDA5050.Order;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Visualization;
|
||||
using RobotNet10.Common;
|
||||
using RobotNet10.Common.Models;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.FleetManager.Services.RobotManager;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl.Models;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Robot;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RobotNet10.FleetManager.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// Hosted service for broadcasting robot state and visualization updates via SignalR.
|
||||
/// Broadcasts updates at 1Hz (every 1 second) to all subscribed clients.
|
||||
/// Also manages LevelId subscriptions for Monitor page.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This service runs as a BackgroundService and periodically broadcasts
|
||||
/// StateMsg and VisualizationMsg updates from RobotManagerService to subscribed clients.
|
||||
/// </remarks>
|
||||
public class RobotStateHubContext(
|
||||
IHubContext<RobotStateHub> hubContext,
|
||||
IRobotManagerService robotManagerService,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
Services.Logger<RobotStateHubContext> logger,
|
||||
IOrderControlService orderControlService,
|
||||
ILoggerFactory loggerFactory) : BackgroundService
|
||||
{
|
||||
private readonly IHubContext<RobotStateHub> _hubContext = hubContext;
|
||||
private readonly IRobotManagerService _robotManagerService = robotManagerService;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory = serviceScopeFactory;
|
||||
private readonly Services.Logger<RobotStateHubContext> _logger = logger;
|
||||
private readonly ILoggerFactory _loggerFactory = loggerFactory;
|
||||
private readonly IOrderControlService _orderControlService = orderControlService;
|
||||
private WatchTimerAsync<RobotStateHubContext>? _broadcastTimer;
|
||||
private const int BroadcastIntervalMs = 500; // 1Hz = 1000ms
|
||||
|
||||
// ===== MONITOR LEVEL SUBSCRIPTION MANAGEMENT =====
|
||||
private readonly ConcurrentDictionary<Guid, Queue<string>> _levelSubscriptions = new();
|
||||
private readonly ConcurrentDictionary<string, Guid> _connectionToLevel = new();
|
||||
private readonly Lock _subscriptionLock = new();
|
||||
private const int MaxConnectionsPerLevel = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe a connection to a levelId for monitor updates.
|
||||
/// If connection already subscribed to another level, unsubscribe from old level first.
|
||||
/// If level has 5 connections, the oldest connection will be evicted.
|
||||
/// </summary>
|
||||
/// <returns>ConnectionId that was evicted (if any), null otherwise</returns>
|
||||
public string? SubscribeToLevel(string connectionId, Guid levelId)
|
||||
{
|
||||
lock (_subscriptionLock)
|
||||
{
|
||||
// If connection already subscribed to another level, unsubscribe first
|
||||
if (_connectionToLevel.TryGetValue(connectionId, out var oldLevelId))
|
||||
{
|
||||
if (oldLevelId == levelId)
|
||||
{
|
||||
// Already subscribed to this level, no change needed
|
||||
return null;
|
||||
}
|
||||
UnsubscribeFromLevelInternal(connectionId, oldLevelId);
|
||||
}
|
||||
|
||||
// Get or create queue for this level
|
||||
var queue = _levelSubscriptions.GetOrAdd(levelId, _ => new Queue<string>());
|
||||
|
||||
// If queue is full, evict oldest connection
|
||||
string? evictedConnectionId = null;
|
||||
if (queue.Count >= MaxConnectionsPerLevel)
|
||||
{
|
||||
evictedConnectionId = queue.Dequeue();
|
||||
_connectionToLevel.TryRemove(evictedConnectionId, out _);
|
||||
}
|
||||
|
||||
// Add new connection to queue
|
||||
queue.Enqueue(connectionId);
|
||||
_connectionToLevel[connectionId] = levelId;
|
||||
|
||||
return evictedConnectionId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe a connection from its current level
|
||||
/// </summary>
|
||||
public bool UnsubscribeFromLevel(string connectionId)
|
||||
{
|
||||
lock (_subscriptionLock)
|
||||
{
|
||||
if (!_connectionToLevel.TryGetValue(connectionId, out var levelId))
|
||||
{
|
||||
return false; // Not subscribed
|
||||
}
|
||||
|
||||
return UnsubscribeFromLevelInternal(connectionId, levelId);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UnsubscribeFromLevelInternal(string connectionId, Guid levelId)
|
||||
{
|
||||
if (!_levelSubscriptions.TryGetValue(levelId, out var queue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove connection from queue
|
||||
var tempQueue = new Queue<string>();
|
||||
bool found = false;
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var id = queue.Dequeue();
|
||||
if (id != connectionId)
|
||||
{
|
||||
tempQueue.Enqueue(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace queue
|
||||
while (tempQueue.Count > 0)
|
||||
{
|
||||
queue.Enqueue(tempQueue.Dequeue());
|
||||
}
|
||||
|
||||
// Remove connection mapping
|
||||
_connectionToLevel.TryRemove(connectionId, out _);
|
||||
|
||||
// If queue is empty, remove level entry
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
_levelSubscriptions.TryRemove(levelId, out _);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all connectionIds subscribed to a levelId
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> GetConnectionsForLevel(Guid levelId)
|
||||
{
|
||||
lock (_subscriptionLock)
|
||||
{
|
||||
if (!_levelSubscriptions.TryGetValue(levelId, out var queue))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return [.. queue];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get levelId that a connection is subscribed to
|
||||
/// </summary>
|
||||
public Guid? GetLevelForConnection(string connectionId)
|
||||
{
|
||||
_connectionToLevel.TryGetValue(connectionId, out var levelId);
|
||||
return levelId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove connection (called on disconnect)
|
||||
/// </summary>
|
||||
public void RemoveConnection(string connectionId)
|
||||
{
|
||||
UnsubscribeFromLevel(connectionId);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
// Start broadcast timer at 1Hz
|
||||
_broadcastTimer = new WatchTimerAsync<RobotStateHubContext>(
|
||||
BroadcastIntervalMs,
|
||||
BroadcastAllRobots,
|
||||
_loggerFactory.CreateLogger<RobotStateHubContext>()
|
||||
);
|
||||
_broadcastTimer.Start();
|
||||
_logger.Info("Started robot state broadcast service at 1Hz");
|
||||
}
|
||||
|
||||
private async Task BroadcastAllRobots()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get all robot data from RobotManagerService
|
||||
var allRobotData = _robotManagerService.GetAllRobotData();
|
||||
|
||||
foreach (var (robotId, robotData) in allRobotData)
|
||||
{
|
||||
// Broadcast State if available
|
||||
if (robotData.State != null)
|
||||
{
|
||||
await BroadcastStateUpdate(robotId, robotData.State);
|
||||
}
|
||||
|
||||
// Broadcast Visualization if available
|
||||
if (robotData.Visualization != null && robotData.State != null)
|
||||
{
|
||||
await BroadcastMonitorDataUpdate(robotId, robotData.Visualization, robotData.State);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error in broadcast cycle: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast state update to all clients subscribed to the robot
|
||||
/// </summary>
|
||||
/// <param name="robotId">Robot identifier (serialNumber)</param>
|
||||
/// <param name="state">VDA5050 State message</param>
|
||||
private async Task BroadcastStateUpdate(string robotId, StateMsg state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var groupName = $"robot:{robotId}";
|
||||
await _hubContext.Clients.Group(groupName).SendAsync("OnStateUpdate", state);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error broadcasting state update for robot {robotId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast visualization update to clients subscribed to the robot's levelId (for Monitor)
|
||||
/// Only broadcasts to connections that have subscribed to the robot's levelId.
|
||||
/// </summary>
|
||||
/// <param name="robotId">Robot identifier (serialNumber)</param>
|
||||
/// <param name="visualization">VDA5050 Visualization message</param>
|
||||
/// <param name="state">VDA5050 State message</param>
|
||||
private async Task BroadcastMonitorDataUpdate(string robotId, VisualizationMsg? visualization, StateMsg? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (visualization is null || state is null) return;
|
||||
|
||||
// Get robot's MapId (LevelId) from database
|
||||
Guid? levelId = await GetRobotLevelIdAsync(robotId);
|
||||
if (!levelId.HasValue)
|
||||
{
|
||||
// Robot has no MapId assigned, skip broadcast
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all connections subscribed to this levelId
|
||||
var connectionIds = GetConnectionsForLevel(levelId.Value);
|
||||
if (connectionIds.Count == 0)
|
||||
{
|
||||
// No connections subscribed to this levelId, skip broadcast
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get robot path
|
||||
var robotPath = GetRobotPath(robotId, visualization.AgvPosition.X, visualization.AgvPosition.Y, state.NodeStates, state.LastNodeId);
|
||||
|
||||
// Prepare broadcast data
|
||||
RobotMonitorBoardcastData data = new()
|
||||
{
|
||||
RobotId = robotId,
|
||||
AgvPosition = visualization.AgvPosition,
|
||||
AgvVelocity = visualization.Velocity,
|
||||
Battery = state.BatteryState,
|
||||
Errors = state.Errors,
|
||||
Infomations = state.Information,
|
||||
Loads = state.Loads,
|
||||
Path = robotPath,
|
||||
};
|
||||
|
||||
// Broadcast to specific connections (not groups)
|
||||
await _hubContext.Clients.Clients(connectionIds).SendAsync("OnMonitorUpdate", data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error broadcasting visualization update for robot {robotId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot's LevelId (MapId) from database
|
||||
/// </summary>
|
||||
private async Task<Guid?> GetRobotLevelIdAsync(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var robotService = scope.ServiceProvider.GetRequiredService<IRobotService>();
|
||||
var robot = await robotService.GetByRobotIdAsync(robotId);
|
||||
return robot?.MapId; // MapId is the LevelId
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting LevelId for robot {robotId}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Split edge at robot's current position, returning remaining edge segment(s)
|
||||
/// </summary>
|
||||
private static NavigationPathEdge[] SplitChecking(double robotX, double robotY, Node lastNode, Node nearLastNode, Edge edge)
|
||||
{
|
||||
List<NavigationPathEdge> pathEdges = [];
|
||||
|
||||
var spaceEdge = new SpaceEdge
|
||||
{
|
||||
StartX = lastNode.NodePosition?.X ?? 0,
|
||||
StartY = lastNode.NodePosition?.Y ?? 0,
|
||||
EndX = nearLastNode.NodePosition?.X ?? 0,
|
||||
EndY = nearLastNode.NodePosition?.Y ?? 0,
|
||||
Degree = edge.Trajectory?.Degree ?? 1,
|
||||
ControlPoint1X = edge.Trajectory?.ControlPoints.Length > 1 ? edge.Trajectory.ControlPoints[1].X : 0,
|
||||
ControlPoint1Y = edge.Trajectory?.ControlPoints.Length > 1 ? edge.Trajectory.ControlPoints[1].Y : 0,
|
||||
ControlPoint2X = edge.Trajectory?.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[2].X : 0,
|
||||
ControlPoint2Y = edge.Trajectory?.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[2].Y : 0,
|
||||
};
|
||||
|
||||
// Get projection point on edge and the time parameter
|
||||
var (projX, projY, _, projTime) = SpaceCompute.GetProjectionOnEdge(robotX, robotY, spaceEdge);
|
||||
|
||||
// Clamp projTime to [0, 1]
|
||||
projTime = Math.Max(0, Math.Min(1, projTime));
|
||||
|
||||
// If edge is degree 1, create a single linear edge
|
||||
if (spaceEdge.Degree == 1)
|
||||
{
|
||||
var linearEdge = new NavigationPathEdge
|
||||
{
|
||||
StartX = projX,
|
||||
StartY = projY,
|
||||
EndX = nearLastNode.NodePosition?.X ?? 0,
|
||||
EndY = nearLastNode.NodePosition?.Y ?? 0,
|
||||
Degree = 1
|
||||
};
|
||||
pathEdges.Add(linearEdge);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For degree 2 or 3: sample points along the curve from projection point to end
|
||||
// Resolution: 0.2m per segment
|
||||
const double resolution = 0.2;
|
||||
|
||||
// Sample points along the curve
|
||||
List<(double x, double y)> samplePoints = [];
|
||||
samplePoints.Add((projX, projY)); // Start from projection point
|
||||
|
||||
double currentTime = projTime;
|
||||
var prevPoint = new SpaceNode(projX, projY);
|
||||
|
||||
while (currentTime < 1.0)
|
||||
{
|
||||
// Find next sample point at resolution distance along the curve
|
||||
double nextTime = FindNextSampleTime(spaceEdge, currentTime, resolution);
|
||||
nextTime = Math.Min(1.0, nextTime);
|
||||
|
||||
var nextPoint = SpaceCompute.BezierPoint(nextTime, spaceEdge);
|
||||
|
||||
// Check if we've moved enough distance
|
||||
double segmentLength = Math.Sqrt(Math.Pow(nextPoint.X - prevPoint.X, 2) +
|
||||
Math.Pow(nextPoint.Y - prevPoint.Y, 2));
|
||||
|
||||
if (segmentLength >= resolution * 0.5 || nextTime >= 1.0)
|
||||
{
|
||||
samplePoints.Add((nextPoint.X, nextPoint.Y));
|
||||
prevPoint = nextPoint;
|
||||
currentTime = nextTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If segment is too short, advance time slightly and try again
|
||||
currentTime = Math.Min(1.0, currentTime + 0.01);
|
||||
}
|
||||
|
||||
// Safety check to avoid infinite loop
|
||||
if (nextTime >= 1.0 || currentTime >= 1.0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the last point is the end node
|
||||
if (samplePoints.Count == 0 ||
|
||||
Math.Abs(samplePoints[^1].x - nearLastNode.NodePosition?.X ?? 0) > 1e-6 ||
|
||||
Math.Abs(samplePoints[^1].y - nearLastNode.NodePosition?.Y ?? 0) > 1e-6)
|
||||
{
|
||||
samplePoints.Add((nearLastNode.NodePosition?.X ?? 0, nearLastNode.NodePosition?.Y ?? 0));
|
||||
}
|
||||
|
||||
// Create linear edges between consecutive sample points
|
||||
for (int i = 0; i < samplePoints.Count - 1; i++)
|
||||
{
|
||||
var linearEdge = new NavigationPathEdge
|
||||
{
|
||||
StartX = samplePoints[i].x,
|
||||
StartY = samplePoints[i].y,
|
||||
EndX = samplePoints[i + 1].x,
|
||||
EndY = samplePoints[i + 1].y,
|
||||
Degree = 1
|
||||
};
|
||||
pathEdges.Add(linearEdge);
|
||||
}
|
||||
}
|
||||
|
||||
return [.. pathEdges];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find next sample time at approximately resolution distance from current time along the curve
|
||||
/// </summary>
|
||||
private static double FindNextSampleTime(SpaceEdge edge, double currentTime, double resolution)
|
||||
{
|
||||
// Use binary search to find the time where distance from current point is approximately resolution
|
||||
double low = currentTime;
|
||||
double high = 1.0;
|
||||
double targetDistance = resolution;
|
||||
double tolerance = 0.01; // 1cm tolerance
|
||||
|
||||
var startPoint = SpaceCompute.BezierPoint(currentTime, edge);
|
||||
|
||||
// Binary search
|
||||
for (int iter = 0; iter < 20; iter++)
|
||||
{
|
||||
double mid = (low + high) / 2;
|
||||
var midPoint = SpaceCompute.BezierPoint(mid, edge);
|
||||
double distance = Math.Sqrt(Math.Pow(midPoint.X - startPoint.X, 2) +
|
||||
Math.Pow(midPoint.Y - startPoint.Y, 2));
|
||||
|
||||
if (Math.Abs(distance - targetDistance) < tolerance)
|
||||
{
|
||||
return mid;
|
||||
}
|
||||
|
||||
if (distance < targetDistance)
|
||||
{
|
||||
low = mid;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return (low + high) / 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot navigation path (Base and Full) from current position
|
||||
/// </summary>
|
||||
private NavigationPath GetRobotPath(string robotId, double robotX, double robotY, NodeState[] nodestates, string lastNodeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robotRoute = _orderControlService.GetRobotRoute(robotId);
|
||||
if (robotRoute is null || robotRoute.FullRoute.Count == 0)
|
||||
{
|
||||
return new NavigationPath { NavigationState = "NoRoute" };
|
||||
}
|
||||
|
||||
if (nodestates.Length == 0 || string.IsNullOrEmpty(lastNodeId))
|
||||
{
|
||||
return new NavigationPath { NavigationState = "NoRoute" };
|
||||
}
|
||||
|
||||
var fullRoute = robotRoute.FullRoute;
|
||||
|
||||
// Find last node segment index
|
||||
var lastNodeIndex = fullRoute.FindIndex(r => r.VdaNode != null && r.VdaNode.NodeId == lastNodeId);
|
||||
if (lastNodeIndex == -1)
|
||||
{
|
||||
// Last node not found, return full route from start
|
||||
return ConvertRouteToNavigationPath(robotRoute, 0, false, robotX, robotY);
|
||||
}
|
||||
|
||||
// Check if robot is on an edge (between lastNodeIndex and next node)
|
||||
bool isOnEdge = false;
|
||||
int startIndex = lastNodeIndex;
|
||||
|
||||
// Check if there's an edge segment after last node
|
||||
if (lastNodeIndex + 1 < fullRoute.Count && fullRoute[lastNodeIndex + 1].IsEdge)
|
||||
{
|
||||
var edgeSegment = fullRoute[lastNodeIndex + 1];
|
||||
var lastNode = fullRoute[lastNodeIndex].VdaNode!;
|
||||
|
||||
// Find next node segment
|
||||
var nextNodeIndex = lastNodeIndex + 2;
|
||||
if (nextNodeIndex < fullRoute.Count && fullRoute[nextNodeIndex].IsNode)
|
||||
{
|
||||
var nextNode = fullRoute[nextNodeIndex].VdaNode!;
|
||||
|
||||
// Check if robot is between last node and next node (on the edge)
|
||||
// Simple check: if robot is closer to edge than to either node
|
||||
var distToLastNode = Math.Sqrt(Math.Pow(robotX - lastNode.NodePosition?.X ?? 0, 2) + Math.Pow(robotY - lastNode.NodePosition?.Y ?? 0, 2));
|
||||
var distToNextNode = Math.Sqrt(Math.Pow(robotX - nextNode.NodePosition?.X ?? 0, 2) + Math.Pow(robotY - nextNode.NodePosition?.Y ?? 0, 2));
|
||||
|
||||
// Get edge length
|
||||
var edgeLength = Math.Sqrt(Math.Pow(nextNode.NodePosition?.X ?? 0 - lastNode.NodePosition?.X ?? 0, 2) +
|
||||
Math.Pow(nextNode.NodePosition?.Y ?? 0 - lastNode.NodePosition?.Y ?? 0, 2));
|
||||
|
||||
// If robot is closer to edge than to nodes, and not too far from edge
|
||||
if (edgeLength > 0.1 && distToLastNode > 0.1 && distToNextNode > 0.1)
|
||||
{
|
||||
isOnEdge = true;
|
||||
startIndex = lastNodeIndex + 1; // Start from edge segment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert route to navigation path starting from startIndex
|
||||
return ConvertRouteToNavigationPath(robotRoute, startIndex, isOnEdge, robotX, robotY, lastNodeIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting robot path for {robotId}: {ex.Message}");
|
||||
return new NavigationPath { NavigationState = "Error" };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert RobotRoute segments to NavigationPath (Base and Full)
|
||||
/// </summary>
|
||||
private static NavigationPath ConvertRouteToNavigationPath(
|
||||
RobotRoute robotRoute,
|
||||
int startIndex,
|
||||
bool isOnEdge,
|
||||
double robotX,
|
||||
double robotY,
|
||||
int? lastNodeIndex = null)
|
||||
{
|
||||
var fullPath = new List<NavigationPathEdge>();
|
||||
var basePath = new List<NavigationPathEdge>();
|
||||
var fullRoute = robotRoute.FullRoute;
|
||||
|
||||
if (startIndex >= fullRoute.Count)
|
||||
{
|
||||
return new NavigationPath { NavigationState = "Complete" };
|
||||
}
|
||||
|
||||
// If robot is on edge, split the edge
|
||||
if (isOnEdge && startIndex < fullRoute.Count && fullRoute[startIndex].IsEdge)
|
||||
{
|
||||
var edgeSegment = fullRoute[startIndex];
|
||||
if (lastNodeIndex.HasValue && lastNodeIndex.Value >= 0 && lastNodeIndex.Value < fullRoute.Count)
|
||||
{
|
||||
var lastNode = fullRoute[lastNodeIndex.Value].VdaNode;
|
||||
var nextNodeIndex = startIndex + 1;
|
||||
if (nextNodeIndex < fullRoute.Count && fullRoute[nextNodeIndex].IsNode && lastNode != null)
|
||||
{
|
||||
var nextNode = fullRoute[nextNodeIndex].VdaNode;
|
||||
if (nextNode != null && edgeSegment.VdaEdge != null)
|
||||
{
|
||||
// Split edge at robot position
|
||||
var splitEdges = SplitChecking(robotX, robotY, lastNode, nextNode, edgeSegment.VdaEdge);
|
||||
fullPath.AddRange(splitEdges);
|
||||
|
||||
// Add to base path if segment is released
|
||||
if (edgeSegment.Released)
|
||||
{
|
||||
basePath.AddRange(splitEdges);
|
||||
}
|
||||
|
||||
startIndex = nextNodeIndex + 1; // Move to segment after next node (skip the edge and next node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!isOnEdge && startIndex < fullRoute.Count && fullRoute[startIndex].IsNode)
|
||||
{
|
||||
// Robot is at a node, skip the node segment and start from next edge
|
||||
startIndex += 2;
|
||||
}
|
||||
|
||||
// Convert remaining segments
|
||||
for (int i = startIndex; i < fullRoute.Count; i++)
|
||||
{
|
||||
var segment = fullRoute[i];
|
||||
|
||||
if (segment.IsEdge && segment.VdaEdge != null)
|
||||
{
|
||||
// Find start and end nodes for this edge
|
||||
Node? startNode = null;
|
||||
Node? endNode = null;
|
||||
|
||||
// Start node: previous segment should be a node
|
||||
if (i > 0 && fullRoute[i - 1].IsNode)
|
||||
{
|
||||
startNode = fullRoute[i - 1].VdaNode;
|
||||
}
|
||||
|
||||
// End node: next segment should be a node
|
||||
if (i + 1 < fullRoute.Count && fullRoute[i + 1].IsNode)
|
||||
{
|
||||
endNode = fullRoute[i + 1].VdaNode;
|
||||
}
|
||||
|
||||
if (startNode != null && endNode != null)
|
||||
{
|
||||
var edge = ConvertEdgeSegmentToNavigationPathEdge(segment, startNode, endNode);
|
||||
if (edge != null)
|
||||
{
|
||||
fullPath.Add(edge);
|
||||
if (segment.Released)
|
||||
{
|
||||
basePath.Add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: Node segments don't create NavigationPathEdge, only edges do
|
||||
}
|
||||
|
||||
return new NavigationPath
|
||||
{
|
||||
NavigationState = robotRoute.Base.Count > 0 ? "Active" : "Planning",
|
||||
RobotPath = [.. fullPath],
|
||||
RobotBasePath = [.. basePath]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert RouteSegment (Edge) to NavigationPathEdge
|
||||
/// </summary>
|
||||
private static NavigationPathEdge? ConvertEdgeSegmentToNavigationPathEdge(RouteSegment segment, Node? startNode, Node? endNode)
|
||||
{
|
||||
if (!segment.IsEdge || segment.VdaEdge == null) return null;
|
||||
|
||||
var edge = segment.VdaEdge;
|
||||
var trajectory = edge.Trajectory;
|
||||
|
||||
// Get start and end positions from node positions (not from trajectory control points)
|
||||
if (startNode == null || endNode == null) return null;
|
||||
|
||||
var navEdge = new NavigationPathEdge
|
||||
{
|
||||
StartX = startNode.NodePosition?.X ?? 0,
|
||||
StartY = startNode.NodePosition?.Y ?? 0,
|
||||
EndX = endNode.NodePosition?.X ?? 0,
|
||||
EndY = endNode.NodePosition?.Y ?? 0,
|
||||
Degree = trajectory?.Degree ?? 1,
|
||||
};
|
||||
|
||||
// Set control points from trajectory (if available)
|
||||
if (trajectory != null && trajectory.ControlPoints.Length > 1)
|
||||
{
|
||||
// ControlPoints[1] is first control point, ControlPoints[2] is second control point (for cubic bezier)
|
||||
if (trajectory.ControlPoints.Length > 1)
|
||||
{
|
||||
navEdge.ControlPoint1X = trajectory.ControlPoints[1].X;
|
||||
navEdge.ControlPoint1Y = trajectory.ControlPoints[1].Y;
|
||||
}
|
||||
|
||||
if (trajectory.ControlPoints.Length > 2)
|
||||
{
|
||||
navEdge.ControlPoint2X = trajectory.ControlPoints[2].X;
|
||||
navEdge.ControlPoint2Y = trajectory.ControlPoints[2].Y;
|
||||
}
|
||||
}
|
||||
|
||||
return navEdge;
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_broadcastTimer?.Dispose();
|
||||
_logger.Info("Stopped robot state broadcast service");
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Script.Shared;
|
||||
|
||||
namespace RobotNet10.FleetManager.Models;
|
||||
|
||||
/// <summary>
|
||||
/// FleetManager Script Globals - Provides APIs for scripts to interact with FleetManager
|
||||
/// </summary>
|
||||
public class FleetManagerScriptGlobals(IRobotManager robotManager, ILayoutManager layoutManager) : IFleetManagerScriptGlobals
|
||||
{
|
||||
public IRobotManager RobotManager { get; } = robotManager;
|
||||
|
||||
public ILayoutManager LayoutManager { get; } = layoutManager;
|
||||
}
|
||||
|
||||
238
srcs/RobotNet10/FleetManager/RobotNet10.FleetManager/Program.cs
Normal file
238
srcs/RobotNet10/FleetManager/RobotNet10.FleetManager/Program.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MudBlazor.Services;
|
||||
using NLog.Web;
|
||||
using RobotNet10.CustomConfiguration.Extensions;
|
||||
using RobotNet10.FleetManager.Client;
|
||||
using RobotNet10.FleetManager.Components;
|
||||
using RobotNet10.FleetManager.Components.Account;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Events;
|
||||
using RobotNet10.FleetManager.Hubs;
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.FleetManager.Services.ConfigManager;
|
||||
using RobotNet10.FleetManager.Services.OpenACS;
|
||||
using RobotNet10.FleetManager.Services.RobotConnections;
|
||||
using RobotNet10.FleetManager.Services.RobotManager;
|
||||
using RobotNet10.FleetManager.Services.Script;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl.ACS;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl.Services;
|
||||
using RobotNet10.GlobalPathPlanner;
|
||||
using RobotNet10.MapManager.Extensions;
|
||||
using RobotNet10.ScriptEngine;
|
||||
using RobotNet10.ScriptEngine.Helpers;
|
||||
using RobotNet10.StorageManager;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Host.UseNLog();
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents()
|
||||
.AddInteractiveWebAssemblyComponents()
|
||||
.AddAuthenticationStateSerialization();
|
||||
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddScoped<IdentityRedirectManager>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
|
||||
|
||||
builder.Services.AddAuthentication()
|
||||
.AddBearerToken(IdentityConstants.BearerScheme);
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
|
||||
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
||||
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
|
||||
{
|
||||
options.SignIn.RequireConfirmedAccount = true;
|
||||
options.Lockout.AllowedForNewUsers = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireDigit = false;
|
||||
})
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
|
||||
|
||||
var mapConnectionString = builder.Configuration.GetConnectionString("MapEditorConnection") ?? throw new InvalidOperationException("Connection string 'MapEditorConnection' not found.");
|
||||
builder.Services.AddMapManager(builder.Configuration, options => options.UseSqlServer(mapConnectionString, b => b.MigrationsAssembly("RobotNet10.FleetManager").UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)), "LayoutImage");
|
||||
|
||||
builder.Services.AddMudServices();
|
||||
builder.Services.AddNavigationMenu(builder.Configuration["APP_VERSION"] ?? "dev");
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// Register VDA5050 Event Bus (must be registered before services that use it)
|
||||
builder.Services.AddSingleton<IRobotEventBus, RobotEventBus>();
|
||||
|
||||
// Register StorageConfig for RobotModelImages
|
||||
builder.Services.Configure<StorageConfig>("RobotModelImages", options =>
|
||||
{
|
||||
builder.Configuration.GetSection("RobotModelImage").Bind(options);
|
||||
});
|
||||
|
||||
// Register Robot Management Services (with event bus for cache invalidation)
|
||||
builder.Services.AddScoped<IRobotModelService, RobotModelService>();
|
||||
builder.Services.AddScoped<IRobotService, RobotService>();
|
||||
builder.Services.AddScoped<IRobotModelImageStorageService, RobotModelImageStorageService>();
|
||||
builder.Services.AddScoped<IRobotModelMapService, RobotModelMapService>();
|
||||
|
||||
// Register SignalR Hub Context as BackgroundService (broadcasts at 1Hz)
|
||||
builder.Services.AddSingleton<RobotStateHubContext>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotStateHubContext>());
|
||||
|
||||
// Register Logger service (factory pattern)
|
||||
builder.Services.AddSingleton(typeof(RobotNet10.FleetManager.Services.Logger<>));
|
||||
|
||||
// Register ConfigManager
|
||||
builder.Services.AddSingleton<IConnectionConfig, ConnectionConfig>();
|
||||
builder.Services.AddSingleton<ITrafficConfig, TrafficConfig>();
|
||||
builder.Services.AddSingleton<IACSTrafficConfig, ACSTrafficConfig>();
|
||||
|
||||
// Register OpenACS Services
|
||||
builder.Services.AddSingleton<TrafficACS>();
|
||||
builder.Services.AddSingleton<OpenACSPublisher>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<OpenACSPublisher>());
|
||||
|
||||
// Register ACS Order Control
|
||||
builder.Services.AddSingleton<IOrderControlService, OrderACSControl>();
|
||||
|
||||
// Note: IRobotEventBus is registered above with Robot Management Services
|
||||
|
||||
// Register VDA5050 RobotConnections Service
|
||||
builder.Services.AddSingleton<IRobotConnectionsService, RobotConnectionsService>();
|
||||
|
||||
// Register VDA5050 RobotManager Service
|
||||
builder.Services.AddSingleton<RobotManagerService>();
|
||||
builder.Services.AddSingleton<IRobotManagerService>(sp => sp.GetRequiredService<RobotManagerService>());
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotManagerService>());
|
||||
|
||||
// Register Path Planner Factory
|
||||
builder.Services.AddSingleton<IPathPlannerFactory, PathPlannerFactory>();
|
||||
|
||||
// Register TrafficControl Sub-Services (order matters due to dependencies)
|
||||
// 1. RouteStorageService - No dependencies
|
||||
builder.Services.AddSingleton<IRouteStorageService, RouteStorageService>();
|
||||
|
||||
// 2. PriorityService - Only needs Logger
|
||||
builder.Services.AddSingleton<IPriorityService, PriorityService>();
|
||||
|
||||
// 3. RobotInfoService - Needs IServiceScopeFactory, IRobotManagerService, IRobotEventBus
|
||||
builder.Services.AddSingleton<IRobotInfoService, RobotInfoService>();
|
||||
|
||||
// 4. EdgeReservationService - Needs INodeService, IEdgeService, IServiceScopeFactory, IRobotInfoService
|
||||
builder.Services.AddSingleton<IEdgeReservationService, EdgeReservationService>();
|
||||
|
||||
// 5. OrderUpdateService - Needs IRobotManagerService
|
||||
builder.Services.AddSingleton<IOrderUpdateService, OrderUpdateService>();
|
||||
|
||||
// 6. ConflictDetectionService - Needs many services
|
||||
builder.Services.AddSingleton<IConflictDetectionService, ConflictDetectionService>();
|
||||
|
||||
// 7. ConflictResolutionService - Needs many services
|
||||
builder.Services.AddSingleton<IConflictResolutionService, ConflictResolutionService>();
|
||||
|
||||
// 8. BaseHorizonManagementService - Needs many services
|
||||
builder.Services.AddSingleton<IBaseHorizonManagementService, BaseHorizonManagementService>();
|
||||
|
||||
// 9. RoutePlanningService - Needs many services
|
||||
builder.Services.AddSingleton<IRoutePlanningService, RoutePlanningService>();
|
||||
|
||||
// Register TrafficControl Service (Orchestrator) - Must be registered after all sub-services
|
||||
builder.Services.AddSingleton<TrafficControlService>();
|
||||
builder.Services.AddSingleton<ITrafficControlService , TrafficControlService>(sp => sp.GetRequiredService<TrafficControlService>());
|
||||
//builder.Services.AddHostedService(sp => sp.GetRequiredService<TrafficControlService>());
|
||||
|
||||
// Add SignalR
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
// Add Custom Configuration
|
||||
builder.Services.AddCustomConfiguration(builder.Configuration, "StorageConfig");
|
||||
|
||||
var scriptConnectionString = builder.Configuration.GetConnectionString("ScriptEngineConnection") ?? throw new InvalidOperationException("Connection string 'ScriptEngineConnection' not found.");
|
||||
builder.Services.AddScriptEngine<ScriptEngineResource>(options => options.UseSqlServer(scriptConnectionString, b => b.MigrationsAssembly("RobotNet10.FleetManager")));
|
||||
|
||||
// Add Script Services
|
||||
builder.Services.AddSingleton<IRobotManager, ScriptRobotmanager>();
|
||||
builder.Services.AddSingleton<ILayoutManager, ScriptLayoutManager>();
|
||||
|
||||
#if DEBUG
|
||||
var dllPath = builder.Configuration["ScriptEngine:RuntimeDllFolder"] ?? "dlls";
|
||||
if (!Directory.Exists(dllPath))
|
||||
{
|
||||
Directory.CreateDirectory(dllPath);
|
||||
}
|
||||
var netCoreDir = Path.GetDirectoryName(typeof(object).Assembly.Location);
|
||||
if (!string.IsNullOrEmpty(netCoreDir))
|
||||
{
|
||||
var coreSrc = Path.Combine(netCoreDir, "System.Private.CoreLib.dll");
|
||||
var runtimeSrc = Path.Combine(netCoreDir, "System.Runtime.dll");
|
||||
var linqSrc = Path.Combine(netCoreDir, "System.Linq.Expressions.dll");
|
||||
var scriptSrc = typeof(RobotNet10.Script.ILogger).Assembly.Location;
|
||||
var fleetSrc = typeof(RobotNet10.FleetManager.Script.IRobot).Assembly.Location;
|
||||
|
||||
var coreDest = Path.Combine(dllPath, "System.Private.CoreLib.dll");
|
||||
var runtimeDest = Path.Combine(dllPath, "System.Runtime.dll");
|
||||
var linqDest = Path.Combine(dllPath, "System.Linq.Expressions.dll");
|
||||
var scriptDest = Path.Combine(dllPath, "RobotNet10.Script.dll");
|
||||
var fleetDest = Path.Combine(dllPath, "RobotNet10.FleetManager.Script.dll");
|
||||
|
||||
if (!File.Exists(coreDest)) File.Copy(coreSrc, coreDest);
|
||||
if (!File.Exists(runtimeDest)) File.Copy(runtimeSrc, runtimeDest);
|
||||
if (!File.Exists(linqDest)) File.Copy(linqSrc, linqDest);
|
||||
|
||||
File.Copy(scriptSrc, scriptDest, true);
|
||||
File.Copy(fleetSrc, fleetDest, true);
|
||||
}
|
||||
#endif
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
await app.Services.SeedApplicationDbAsync();
|
||||
await app.Services.SeedScriptEngineDbAsync();
|
||||
await app.Services.SeedMapManagerAsync();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseWebAssemblyDebugging();
|
||||
app.UseMigrationsEndPoint();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.AddAdditionalAssemblies(typeof(RobotNet10.FleetManager.Client._Imports).Assembly);
|
||||
|
||||
// Map API controllers
|
||||
app.MapControllers();
|
||||
|
||||
// Map SignalR Hubs
|
||||
app.MapHub<RobotStateHub>("/hubs/robot-state");
|
||||
|
||||
// Add additional endpoints required by the Identity /Account Razor components.
|
||||
app.MapAdditionalIdentityEndpoints();
|
||||
|
||||
app.MapScriptEngineHubs();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"workingDirectory": "$(TargetDir)",
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://robotnet10_fleetmanager.dev.localhost:5240",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"workingDirectory": "$(TargetDir)",
|
||||
//"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://robotnet10_fleetmanager.dev.localhost:7139;http://robotnet10_fleetmanager.dev.localhost:5240",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mssql1": {
|
||||
"type": "mssql",
|
||||
"connectionId": "ConnectionStrings:DefaultConnection"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mssql1": {
|
||||
"type": "mssql.local",
|
||||
"connectionId": "ConnectionStrings:DefaultConnection"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>aspnet-RobotNet10_FleetManager-c155333e-f9e9-4e5d-b044-b289ee9762af</UserSecretsId>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<ProjectReference Include="..\..\Commons\RobotNet10.Common\RobotNet10.Common.csproj" />
|
||||
<ProjectReference Include="..\..\Commons\RobotNet10.CustomConfiguration\RobotNet10.CustomConfiguration.csproj" />
|
||||
<ProjectReference Include="..\..\Commons\RobotNet10.GlobalPathPlanner\RobotNet10.GlobalPathPlanner.csproj" />
|
||||
<ProjectReference Include="..\..\Commons\RobotNet10.MapManager\RobotNet10.MapManager.csproj" />
|
||||
<ProjectReference Include="..\..\Commons\RobotNet10.MqttConnection\RobotNet10.MqttConnection.csproj" />
|
||||
<ProjectReference Include="..\..\Commons\RobotNet10.ScriptEngine\RobotNet10.ScriptEngine.csproj" />
|
||||
<ProjectReference Include="..\..\Components\RobotNet10.Components\RobotNet10.Components.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\RobotNet.VDA5050\RobotNet.VDA5050.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\RobotNet10.Shared\RobotNet10.Shared.csproj" />
|
||||
<ProjectReference Include="..\RobotNet10.FleetManager.Client\RobotNet10.FleetManager.Client.csproj" />
|
||||
<ProjectReference Include="..\RobotNet10.FleetManager.Shared\RobotNet10.FleetManager.Shared.csproj" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
|
||||
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\Migrations\Appdb\" />
|
||||
<Folder Include="Data\Migrations\MapDb\" />
|
||||
<Folder Include="Data\Migrations\ScriptDb\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,386 @@
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using RobotNet10.CustomConfiguration.Services;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing ACSTraffic configurations
|
||||
/// </summary>
|
||||
public class ACSTrafficConfig : IACSTrafficConfig
|
||||
{
|
||||
private readonly IConfigManager _configManager;
|
||||
private readonly Logger<ACSTrafficConfig> _logger;
|
||||
|
||||
private readonly Lock _lockObject = new();
|
||||
private bool _configLoaded = false;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when ACSTraffic configuration is changed/reloaded
|
||||
/// </summary>
|
||||
public event EventHandler? ConfigChanged;
|
||||
|
||||
private bool _trafficEnable = false;
|
||||
private int _trafficInterval = 1000;
|
||||
private string _trafficURL = string.Empty;
|
||||
private Dictionary<string, string> _acsZoneMaping = [];
|
||||
private Dictionary<string, string> _acsOutMaping = [];
|
||||
private bool _publishEnable = false;
|
||||
private string _publishURL = string.Empty;
|
||||
private int _publishInterval = 1000;
|
||||
|
||||
private const string ACS_TRAFFIC_CONFIG_TYPE = "ACSTrafficConfig";
|
||||
|
||||
public ACSTrafficConfig(IConfigManager configManager, Logger<ACSTrafficConfig> logger)
|
||||
{
|
||||
_configManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
|
||||
_logger = logger;
|
||||
|
||||
// Subscribe to config changes
|
||||
_configManager.ConfigChanged += OnConfigChanged;
|
||||
}
|
||||
|
||||
public bool TrafficEnable
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigLoaded();
|
||||
return _trafficEnable;
|
||||
}
|
||||
}
|
||||
|
||||
public int TrafficInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigLoaded();
|
||||
return _trafficInterval;
|
||||
}
|
||||
}
|
||||
|
||||
public string TrafficURL
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigLoaded();
|
||||
return _trafficURL;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, string> ACSZoneMaping
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigLoaded();
|
||||
return _acsZoneMaping;
|
||||
}
|
||||
}
|
||||
|
||||
public bool PublishEnable
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigLoaded();
|
||||
return _publishEnable;
|
||||
}
|
||||
}
|
||||
|
||||
public string PublishURL
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigLoaded();
|
||||
return _publishURL;
|
||||
}
|
||||
}
|
||||
|
||||
public int PublishInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigLoaded();
|
||||
return _publishInterval;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, string> ACSOutMaping
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureConfigLoaded();
|
||||
return _acsOutMaping;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureConfigLoaded()
|
||||
{
|
||||
if (!_configLoaded)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_configLoaded)
|
||||
{
|
||||
LoadACSTrafficConfigAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadACSTrafficConfigAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configFile = await _configManager.GetConfigByTypeAsync(ACS_TRAFFIC_CONFIG_TYPE);
|
||||
|
||||
if (configFile == null)
|
||||
{
|
||||
_logger.Warning("ACSTraffic configuration not found, using defaults");
|
||||
_trafficEnable = false;
|
||||
_trafficInterval = 1000;
|
||||
_trafficURL = string.Empty;
|
||||
_acsZoneMaping = [];
|
||||
_acsOutMaping = [];
|
||||
_publishEnable = false;
|
||||
_publishURL = string.Empty;
|
||||
_publishInterval = 1000;
|
||||
}
|
||||
else
|
||||
{
|
||||
MapConfigVariablesToProperties(configFile.Variables);
|
||||
_logger.Info("ACSTraffic configuration loaded successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error loading ACSTraffic configuration, using defaults: {ex.Message}");
|
||||
_trafficEnable = false;
|
||||
_trafficInterval = 1000;
|
||||
_trafficURL = string.Empty;
|
||||
_acsZoneMaping = [];
|
||||
_acsOutMaping = [];
|
||||
_publishEnable = false;
|
||||
_publishURL = string.Empty;
|
||||
_publishInterval = 1000;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_configLoaded = true;
|
||||
// Trigger ConfigChanged event after config is loaded/reloaded
|
||||
OnConfigChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigChanged(object? sender, RobotNet10.CustomConfiguration.Events.ConfigChangedEventArgs e)
|
||||
{
|
||||
// Reload config if our config type changed
|
||||
if (e.ConfigType == ACS_TRAFFIC_CONFIG_TYPE)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_configLoaded = false;
|
||||
}
|
||||
_logger.Info($"ACSTraffic configuration changed ({e.ConfigType}), will reload on next access");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger ConfigChanged event to notify subscribers that config has been reloaded
|
||||
/// </summary>
|
||||
private void OnConfigChanged()
|
||||
{
|
||||
ConfigChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void MapConfigVariablesToProperties(List<ConfigVariable> variables)
|
||||
{
|
||||
foreach (var variable in variables)
|
||||
{
|
||||
if (variable.Value == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
switch (variable.Name)
|
||||
{
|
||||
case nameof(TrafficEnable):
|
||||
_trafficEnable = (bool)ConvertValue(variable.Value, typeof(bool))!;
|
||||
break;
|
||||
case nameof(TrafficInterval):
|
||||
_trafficInterval = (int)ConvertValue(variable.Value, typeof(int))!;
|
||||
break;
|
||||
case nameof(TrafficURL):
|
||||
_trafficURL = (string)ConvertValue(variable.Value, typeof(string))!;
|
||||
break;
|
||||
case nameof(ACSZoneMaping):
|
||||
_acsZoneMaping = (Dictionary<string, string>)ConvertValue(variable.Value, typeof(Dictionary<string, string>))!;
|
||||
break;
|
||||
case nameof(ACSOutMaping):
|
||||
_acsOutMaping = (Dictionary<string, string>)ConvertValue(variable.Value, typeof(Dictionary<string, string>))!;
|
||||
break;
|
||||
case nameof(PublishEnable):
|
||||
_publishEnable = (bool)ConvertValue(variable.Value, typeof(bool))!;
|
||||
break;
|
||||
case nameof(PublishURL):
|
||||
_publishURL = (string)ConvertValue(variable.Value, typeof(string))!;
|
||||
break;
|
||||
case nameof(PublishInterval):
|
||||
_publishInterval = (int)ConvertValue(variable.Value, typeof(int))!;
|
||||
break;
|
||||
default:
|
||||
_logger.Warning($"Unknown variable name: {variable.Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Failed to set property {variable.Name} from variable: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object? ConvertValue(object? value, Type targetType)
|
||||
{
|
||||
if (value == null)
|
||||
return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
|
||||
|
||||
// If value is already of the correct type, return it
|
||||
if (targetType.IsInstanceOfType(value))
|
||||
return value;
|
||||
|
||||
try
|
||||
{
|
||||
// Handle nullable types
|
||||
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||
|
||||
// Convert based on target type
|
||||
if (underlyingType == typeof(string))
|
||||
{
|
||||
return value.ToString() ?? string.Empty;
|
||||
}
|
||||
else if (underlyingType == typeof(int))
|
||||
{
|
||||
if (value is int i) return i;
|
||||
if (int.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
if (value is double d) return (int)d;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to int");
|
||||
}
|
||||
else if (underlyingType == typeof(double))
|
||||
{
|
||||
if (value is double d) return d;
|
||||
if (double.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
if (value is int i) return i;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to double");
|
||||
}
|
||||
else if (underlyingType == typeof(bool))
|
||||
{
|
||||
if (value is bool b) return b;
|
||||
if (bool.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
// Handle numeric values: 0/1, "0"/"1", etc.
|
||||
if (value.ToString()?.Trim() == "0" || value.ToString()?.Trim().ToLower() == "false") return false;
|
||||
if (value.ToString()?.Trim() == "1" || value.ToString()?.Trim().ToLower() == "true") return true;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to bool");
|
||||
}
|
||||
else if (underlyingType.IsEnum)
|
||||
{
|
||||
var enumValue = Enum.Parse(underlyingType, value.ToString() ?? "", true);
|
||||
return enumValue;
|
||||
}
|
||||
else if (underlyingType.IsGenericType && underlyingType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
// Handle Dictionary types - try to parse from JSON string
|
||||
return ConvertDictionary(value, underlyingType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try standard conversion
|
||||
return Convert.ChangeType(value, underlyingType);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Error converting value {value} of type {value.GetType()} to {targetType}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert value to Dictionary type (supports Dictionary<string, string>)
|
||||
/// </summary>
|
||||
private object ConvertDictionary(object value, Type dictionaryType)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get key and value types
|
||||
var genericArgs = dictionaryType.GetGenericArguments();
|
||||
var keyType = genericArgs[0];
|
||||
var valueType = genericArgs[1];
|
||||
|
||||
// If value is already the correct Dictionary type, return it
|
||||
if (dictionaryType.IsInstanceOfType(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// If value is Dictionary<string, object>, try to convert
|
||||
if (value is Dictionary<string, object> stringDict)
|
||||
{
|
||||
var result = Activator.CreateInstance(dictionaryType);
|
||||
var addMethod = dictionaryType.GetMethod("Add");
|
||||
|
||||
if (addMethod != null && result != null)
|
||||
{
|
||||
foreach (var kvp in stringDict)
|
||||
{
|
||||
var key = ConvertValue(kvp.Key, keyType);
|
||||
var val = ConvertValue(kvp.Value, valueType);
|
||||
addMethod.Invoke(result, [key, val]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try parse as JSON string
|
||||
var jsonString = value.ToString();
|
||||
if (!string.IsNullOrEmpty(jsonString))
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = System.Text.Json.JsonDocument.Parse(jsonString);
|
||||
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object)
|
||||
{
|
||||
var result = Activator.CreateInstance(dictionaryType);
|
||||
var addMethod = dictionaryType.GetMethod("Add");
|
||||
|
||||
if (addMethod != null && result != null)
|
||||
{
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
// Convert key
|
||||
var key = ConvertValue(prop.Name, keyType);
|
||||
|
||||
// Convert value
|
||||
var val = ConvertValue(prop.Value.GetString() ?? prop.Value.GetRawText(), valueType);
|
||||
|
||||
addMethod.Invoke(result, [key, val]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Failed to parse Dictionary from JSON string: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// If all else fails, return default (empty dictionary)
|
||||
_logger.Warning($"Cannot convert {value.GetType()} to {dictionaryType}, using default (empty dictionary)");
|
||||
return Activator.CreateInstance(dictionaryType) ?? throw new InvalidOperationException($"Cannot create instance of {dictionaryType}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Error converting Dictionary: {ex.Message}");
|
||||
// Return default (empty dictionary)
|
||||
return Activator.CreateInstance(dictionaryType) ?? throw new InvalidOperationException($"Cannot create instance of {dictionaryType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
using System.Reflection;
|
||||
using RobotNet10.CustomConfiguration.Events;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using RobotNet10.CustomConfiguration.Services;
|
||||
using RobotNet10.FleetManager.Services.RobotConnections.Models;
|
||||
using RobotNet10.MqttConnection;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing application configurations
|
||||
/// </summary>
|
||||
public class ConnectionConfig : IConnectionConfig
|
||||
{
|
||||
private readonly IConfigManager _configManager;
|
||||
private readonly Logger<ConnectionConfig> _logger;
|
||||
|
||||
private readonly Lock _lockObject = new();
|
||||
private VDA5050ProtocolConfig? _vda5050Config;
|
||||
private MQTTConfig? _mqttConfig;
|
||||
private bool _vda5050ConfigLoaded = false;
|
||||
private bool _mqttConfigLoaded = false;
|
||||
|
||||
private const string VDA5050_PROTOCOL_CONFIG_TYPE = "VDA5050ProtocolConfig";
|
||||
private const string MQTT_CONFIG_TYPE = "MQTTConfig";
|
||||
|
||||
public ConnectionConfig(IConfigManager configManager, Logger<ConnectionConfig> logger)
|
||||
{
|
||||
_configManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
|
||||
_logger = logger;
|
||||
|
||||
// Subscribe to config changes
|
||||
_configManager.ConfigChanged += OnConfigChanged;
|
||||
}
|
||||
|
||||
public VDA5050ProtocolConfig GetVDA5050Config()
|
||||
{
|
||||
if (!_vda5050ConfigLoaded)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_vda5050ConfigLoaded)
|
||||
{
|
||||
LoadVDA5050ConfigAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _vda5050Config ?? throw new InvalidOperationException("VDA5050 configuration not loaded");
|
||||
}
|
||||
|
||||
public MQTTConfig GetMqttConfig()
|
||||
{
|
||||
if (!_mqttConfigLoaded)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_mqttConfigLoaded)
|
||||
{
|
||||
LoadMqttConfigAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _mqttConfig ?? throw new InvalidOperationException("MQTT configuration not loaded");
|
||||
}
|
||||
|
||||
private async Task LoadVDA5050ConfigAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configFile = await _configManager.GetConfigByTypeAsync(VDA5050_PROTOCOL_CONFIG_TYPE);
|
||||
|
||||
if (configFile == null)
|
||||
{
|
||||
_logger.Warning("VDA5050 Protocol configuration not found, using defaults");
|
||||
_vda5050Config = new VDA5050ProtocolConfig
|
||||
{
|
||||
Manufacturer = "RobotNet",
|
||||
Version = "2.1.0",
|
||||
TopicPrefix = "uagv/v2"
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_vda5050Config = MapConfigVariablesToObject<VDA5050ProtocolConfig>(configFile.Variables);
|
||||
_logger.Info("VDA5050 Protocol configuration loaded successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error loading VDA5050 Protocol configuration, using defaults: {ex.Message}");
|
||||
_vda5050Config = new VDA5050ProtocolConfig
|
||||
{
|
||||
Manufacturer = "RobotNet",
|
||||
Version = "2.1.0",
|
||||
TopicPrefix = "uagv/v2"
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
_vda5050ConfigLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadMqttConfigAsync()
|
||||
{
|
||||
var configFile = await _configManager.GetConfigByTypeAsync(MQTT_CONFIG_TYPE) ?? throw new InvalidOperationException($"MQTT configuration (ConfigType: {MQTT_CONFIG_TYPE}) not found");
|
||||
_mqttConfig = MapConfigVariablesToObject<MQTTConfig>(configFile.Variables);
|
||||
|
||||
if (string.IsNullOrEmpty(_mqttConfig.Host))
|
||||
{
|
||||
throw new InvalidOperationException("MQTT configuration: Host is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_mqttConfig.ClientId))
|
||||
{
|
||||
throw new InvalidOperationException("MQTT configuration: ClientId is required");
|
||||
}
|
||||
|
||||
_mqttConfigLoaded = true;
|
||||
_logger.Info("MQTT configuration loaded successfully");
|
||||
}
|
||||
|
||||
private void OnConfigChanged(object? sender, ConfigChangedEventArgs e)
|
||||
{
|
||||
// Reload config if it's one of our config types
|
||||
if (e.ConfigType == VDA5050_PROTOCOL_CONFIG_TYPE)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_vda5050ConfigLoaded = false;
|
||||
_vda5050Config = null;
|
||||
}
|
||||
_logger.Info("VDA5050 Protocol configuration changed, will reload on next access");
|
||||
}
|
||||
else if (e.ConfigType == MQTT_CONFIG_TYPE)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_mqttConfigLoaded = false;
|
||||
_mqttConfig = null;
|
||||
}
|
||||
_logger.Info("MQTT configuration changed, will reload on next access");
|
||||
}
|
||||
}
|
||||
|
||||
private T MapConfigVariablesToObject<T>(List<ConfigVariable> variables) where T : new()
|
||||
{
|
||||
var obj = new T();
|
||||
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
// Find variable by exact name match (case-sensitive)
|
||||
var variable = variables.FirstOrDefault(v => v.Name == property.Name);
|
||||
|
||||
if (variable != null && variable.Value != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var convertedValue = ConvertValue(variable.Value, property.PropertyType, variable.Type);
|
||||
property.SetValue(obj, convertedValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Failed to set property {property.Name} from variable {variable.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private object? ConvertValue(object? value, Type targetType, ConfigVariableType variableType)
|
||||
{
|
||||
if (value == null)
|
||||
return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
|
||||
|
||||
// If value is already of the correct type, return it
|
||||
if (targetType.IsInstanceOfType(value))
|
||||
return value;
|
||||
|
||||
try
|
||||
{
|
||||
// Handle nullable types
|
||||
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||
|
||||
// Convert based on target type
|
||||
if (underlyingType == typeof(string))
|
||||
{
|
||||
return value.ToString() ?? string.Empty;
|
||||
}
|
||||
else if (underlyingType == typeof(int))
|
||||
{
|
||||
if (value is int i) return i;
|
||||
if (int.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
if (value is double d) return (int)d;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to int");
|
||||
}
|
||||
else if (underlyingType == typeof(double))
|
||||
{
|
||||
if (value is double d) return d;
|
||||
if (double.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
if (value is int i) return i;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to double");
|
||||
}
|
||||
else if (underlyingType == typeof(bool))
|
||||
{
|
||||
if (value is bool b) return b;
|
||||
if (bool.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
// Handle numeric values: 0/1, "0"/"1", etc.
|
||||
if (value.ToString()?.Trim() == "0" || value.ToString()?.Trim().ToLower() == "false") return false;
|
||||
if (value.ToString()?.Trim() == "1" || value.ToString()?.Trim().ToLower() == "true") return true;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to bool");
|
||||
}
|
||||
else if (underlyingType.IsEnum)
|
||||
{
|
||||
var enumValue = Enum.Parse(underlyingType, value.ToString() ?? "", true);
|
||||
return enumValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try standard conversion
|
||||
return Convert.ChangeType(value, underlyingType);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Error converting value {value} of type {value.GetType()} to {targetType}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace RobotNet10.FleetManager.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing ACSTraffic configurations
|
||||
/// </summary>
|
||||
public interface IACSTrafficConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Event triggered when ACSTraffic configuration is changed/reloaded
|
||||
/// </summary>
|
||||
event EventHandler? ConfigChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Enable ACS Traffic control
|
||||
/// </summary>
|
||||
bool TrafficEnable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Traffic interval time in milliseconds
|
||||
/// </summary>
|
||||
int TrafficInterval { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Traffic URL
|
||||
/// </summary>
|
||||
string TrafficURL { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ACS Zone mapping dictionary
|
||||
/// </summary>
|
||||
Dictionary<string, string> ACSZoneMaping { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ACS Out zone mapping with node
|
||||
/// </summary>
|
||||
Dictionary<string, string> ACSOutMaping { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable publish
|
||||
/// </summary>
|
||||
bool PublishEnable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Publish URL
|
||||
/// </summary>
|
||||
string PublishURL { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Publish interval in milliseconds
|
||||
/// </summary>
|
||||
int PublishInterval { get; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using RobotNet10.FleetManager.Services.RobotConnections.Models;
|
||||
using RobotNet10.MqttConnection;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing application configurations
|
||||
/// </summary>
|
||||
public interface IConnectionConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Get VDA5050 Protocol configuration
|
||||
/// </summary>
|
||||
VDA5050ProtocolConfig GetVDA5050Config();
|
||||
|
||||
/// <summary>
|
||||
/// Get MQTT configuration
|
||||
/// </summary>
|
||||
MQTTConfig GetMqttConfig();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using RobotNet10.FleetManager.Services.TrafficControl.Models;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing TrafficControl configurations
|
||||
/// </summary>
|
||||
public interface ITrafficConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Get TrafficControl configuration
|
||||
/// </summary>
|
||||
TrafficControlConfig GetTrafficControlConfig();
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using System.Reflection;
|
||||
using RobotNet10.CustomConfiguration.Events;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using RobotNet10.CustomConfiguration.Services;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl.Models;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing TrafficControl configurations
|
||||
/// </summary>
|
||||
public class TrafficConfig : ITrafficConfig
|
||||
{
|
||||
private readonly IConfigManager _configManager;
|
||||
private readonly Logger<TrafficConfig> _logger;
|
||||
|
||||
private readonly Lock _lockObject = new();
|
||||
private TrafficControlConfig? _trafficControlConfig;
|
||||
private bool _configLoaded = false;
|
||||
|
||||
private const string CONFLICT_DETECTION_CONFIG_TYPE = "TrafficConflictDetectionConfig";
|
||||
private const string BASE_HORIZON_CONFIG_TYPE = "TrafficBaseHorizonConfig";
|
||||
private const string CONFLICT_RESOLUTION_CONFIG_TYPE = "TrafficConflictResolutionConfig";
|
||||
private const string PRIORITY_CONFIG_TYPE = "TrafficPriorityConfig";
|
||||
private const string PATH_PLANNING_CONFIG_TYPE = "TrafficPathPlanningConfig";
|
||||
|
||||
public TrafficConfig(IConfigManager configManager, Logger<TrafficConfig> logger)
|
||||
{
|
||||
_configManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
|
||||
_logger = logger;
|
||||
|
||||
// Subscribe to config changes
|
||||
_configManager.ConfigChanged += OnConfigChanged;
|
||||
}
|
||||
|
||||
public TrafficControlConfig GetTrafficControlConfig()
|
||||
{
|
||||
if (!_configLoaded)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_configLoaded)
|
||||
{
|
||||
LoadTrafficControlConfigAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _trafficControlConfig ?? throw new InvalidOperationException("TrafficControl configuration not loaded");
|
||||
}
|
||||
|
||||
private async Task LoadTrafficControlConfigAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load all nested configs
|
||||
var conflictDetectionConfig = await LoadNestedConfigAsync<ConflictDetectionConfig>(CONFLICT_DETECTION_CONFIG_TYPE);
|
||||
var baseHorizonConfig = await LoadNestedConfigAsync<BaseHorizonConfig>(BASE_HORIZON_CONFIG_TYPE);
|
||||
var conflictResolutionConfig = await LoadNestedConfigAsync<ConflictResolutionConfig>(CONFLICT_RESOLUTION_CONFIG_TYPE);
|
||||
var priorityConfig = await LoadNestedConfigAsync<PriorityConfig>(PRIORITY_CONFIG_TYPE);
|
||||
var pathPlanningConfig = await LoadNestedConfigAsync<PathPlanningConfig>(PATH_PLANNING_CONFIG_TYPE);
|
||||
|
||||
// Combine into TrafficControlConfig
|
||||
_trafficControlConfig = new TrafficControlConfig
|
||||
{
|
||||
ConflictDetection = conflictDetectionConfig,
|
||||
BaseHorizon = baseHorizonConfig,
|
||||
ConflictResolution = conflictResolutionConfig,
|
||||
Priority = priorityConfig,
|
||||
PathPlanning = pathPlanningConfig
|
||||
};
|
||||
|
||||
_configLoaded = true;
|
||||
_logger.Info("TrafficControl configuration loaded successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error loading TrafficControl configuration, using defaults: {ex.Message}");
|
||||
_trafficControlConfig = new TrafficControlConfig();
|
||||
_configLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<T> LoadNestedConfigAsync<T>(string configType) where T : new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configFile = await _configManager.GetConfigByTypeAsync(configType);
|
||||
|
||||
if (configFile == null)
|
||||
{
|
||||
_logger.Warning($"{configType} configuration not found, using defaults");
|
||||
return new T();
|
||||
}
|
||||
|
||||
var config = MapConfigVariablesToObject<T>(configFile.Variables);
|
||||
_logger.Info($"{configType} configuration loaded successfully");
|
||||
return config;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Error loading {configType} configuration, using defaults: {ex.Message}");
|
||||
return new T();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigChanged(object? sender, ConfigChangedEventArgs e)
|
||||
{
|
||||
// Reload config if any of our config types changed
|
||||
if (e.ConfigType == CONFLICT_DETECTION_CONFIG_TYPE ||
|
||||
e.ConfigType == BASE_HORIZON_CONFIG_TYPE ||
|
||||
e.ConfigType == CONFLICT_RESOLUTION_CONFIG_TYPE ||
|
||||
e.ConfigType == PRIORITY_CONFIG_TYPE ||
|
||||
e.ConfigType == PATH_PLANNING_CONFIG_TYPE)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_configLoaded = false;
|
||||
_trafficControlConfig = null;
|
||||
}
|
||||
_logger.Info($"TrafficControl configuration changed ({e.ConfigType}), will reload on next access");
|
||||
}
|
||||
}
|
||||
|
||||
private T MapConfigVariablesToObject<T>(List<ConfigVariable> variables) where T : new()
|
||||
{
|
||||
var obj = new T();
|
||||
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
// Find variable by exact name match (case-sensitive)
|
||||
var variable = variables.FirstOrDefault(v => v.Name == property.Name);
|
||||
|
||||
if (variable != null && variable.Value != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var convertedValue = ConvertValue(variable.Value, property.PropertyType);
|
||||
property.SetValue(obj, convertedValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Failed to set property {property.Name} from variable {variable.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private object? ConvertValue(object? value, Type targetType)
|
||||
{
|
||||
if (value == null)
|
||||
return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
|
||||
|
||||
// If value is already of the correct type, return it
|
||||
if (targetType.IsInstanceOfType(value))
|
||||
return value;
|
||||
|
||||
try
|
||||
{
|
||||
// Handle nullable types
|
||||
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||
|
||||
// Convert based on target type
|
||||
if (underlyingType == typeof(string))
|
||||
{
|
||||
return value.ToString() ?? string.Empty;
|
||||
}
|
||||
else if (underlyingType == typeof(int))
|
||||
{
|
||||
if (value is int i) return i;
|
||||
if (int.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
if (value is double d) return (int)d;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to int");
|
||||
}
|
||||
else if (underlyingType == typeof(double))
|
||||
{
|
||||
if (value is double d) return d;
|
||||
if (double.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
if (value is int i) return i;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to double");
|
||||
}
|
||||
else if (underlyingType == typeof(bool))
|
||||
{
|
||||
if (value is bool b) return b;
|
||||
if (bool.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
// Handle numeric values: 0/1, "0"/"1", etc.
|
||||
if (value.ToString()?.Trim() == "0" || value.ToString()?.Trim().ToLower() == "false") return false;
|
||||
if (value.ToString()?.Trim() == "1" || value.ToString()?.Trim().ToLower() == "true") return true;
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to bool");
|
||||
}
|
||||
else if (underlyingType.IsEnum)
|
||||
{
|
||||
var enumValue = Enum.Parse(underlyingType, value.ToString() ?? "", true);
|
||||
return enumValue;
|
||||
}
|
||||
else if (underlyingType.IsGenericType && underlyingType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
// Handle Dictionary types - try to parse from JSON string
|
||||
return ConvertDictionary(value, underlyingType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try standard conversion
|
||||
return Convert.ChangeType(value, underlyingType);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Error converting value {value} of type {value.GetType()} to {targetType}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert value to Dictionary type (supports Dictionary<NavigationType, PathPlanningMethod>)
|
||||
/// </summary>
|
||||
private object ConvertDictionary(object value, Type dictionaryType)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get key and value types
|
||||
var genericArgs = dictionaryType.GetGenericArguments();
|
||||
var keyType = genericArgs[0];
|
||||
var valueType = genericArgs[1];
|
||||
|
||||
// If value is already the correct Dictionary type, return it
|
||||
if (dictionaryType.IsInstanceOfType(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// If value is Dictionary<string, object>, try to convert
|
||||
if (value is Dictionary<string, object> stringDict)
|
||||
{
|
||||
var result = Activator.CreateInstance(dictionaryType);
|
||||
var addMethod = dictionaryType.GetMethod("Add");
|
||||
|
||||
if (addMethod != null && result != null)
|
||||
{
|
||||
foreach (var kvp in stringDict)
|
||||
{
|
||||
var key = ConvertValue(kvp.Key, keyType);
|
||||
var val = ConvertValue(kvp.Value, valueType);
|
||||
addMethod.Invoke(result, [key, val]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try parse as JSON string
|
||||
var jsonString = value.ToString();
|
||||
if (!string.IsNullOrEmpty(jsonString))
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = System.Text.Json.JsonDocument.Parse(jsonString);
|
||||
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object)
|
||||
{
|
||||
var result = Activator.CreateInstance(dictionaryType);
|
||||
var addMethod = dictionaryType.GetMethod("Add");
|
||||
|
||||
if (addMethod != null && result != null)
|
||||
{
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
// Convert key (e.g., "Differential" -> NavigationType.Differential)
|
||||
var key = ConvertEnumKey(prop.Name, keyType);
|
||||
|
||||
// Convert value (e.g., "Basic" -> PathPlanningMethod.Basic)
|
||||
var val = ConvertEnumValue(prop.Value.GetString() ?? prop.Value.GetRawText(), valueType);
|
||||
|
||||
addMethod.Invoke(result, [key, val]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Failed to parse Dictionary from JSON string: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// If all else fails, return default (empty dictionary)
|
||||
_logger.Warning($"Cannot convert {value.GetType()} to {dictionaryType}, using default (empty dictionary)");
|
||||
return Activator.CreateInstance(dictionaryType) ?? throw new InvalidOperationException($"Cannot create instance of {dictionaryType}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Error converting Dictionary: {ex.Message}");
|
||||
// Return default (empty dictionary)
|
||||
return Activator.CreateInstance(dictionaryType) ?? throw new InvalidOperationException($"Cannot create instance of {dictionaryType}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert string to enum key (e.g., "Differential" -> NavigationType.Differential)
|
||||
/// </summary>
|
||||
private static object ConvertEnumKey(string keyString, Type enumType)
|
||||
{
|
||||
if (enumType.IsEnum)
|
||||
{
|
||||
if (Enum.TryParse(enumType, keyString, true, out var enumValue))
|
||||
{
|
||||
return enumValue;
|
||||
}
|
||||
}
|
||||
// If not enum, try standard conversion
|
||||
return Convert.ChangeType(keyString, enumType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert string to enum value (e.g., "Basic" -> PathPlanningMethod.Basic)
|
||||
/// </summary>
|
||||
private static object ConvertEnumValue(string valueString, Type enumType)
|
||||
{
|
||||
if (enumType.IsEnum)
|
||||
{
|
||||
if (Enum.TryParse(enumType, valueString, true, out var enumValue))
|
||||
{
|
||||
return enumValue;
|
||||
}
|
||||
}
|
||||
// If not enum, try standard conversion
|
||||
return Convert.ChangeType(valueString, enumType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for robot model image storage operations
|
||||
/// </summary>
|
||||
public interface IRobotModelImageStorageService
|
||||
{
|
||||
Task SaveImageAsync(Guid robotModelId, Stream imageStream, CancellationToken cancellationToken = default);
|
||||
Task<Stream?> GetImageAsync(Guid robotModelId, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteImageAsync(Guid robotModelId, CancellationToken cancellationToken = default);
|
||||
Task<bool> ImageExistsAsync(Guid robotModelId, CancellationToken cancellationToken = default);
|
||||
Task<(int width, int height)> GetImageDimensionsAsync(Stream imageStream, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using RobotNet10.FleetManager.Shared.DTOs.RobotModel;
|
||||
using RobotNet10.MapManager.Data;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing robot model map data based on VehicleType filtering
|
||||
/// </summary>
|
||||
public interface IRobotModelMapService
|
||||
{
|
||||
/// <summary>
|
||||
/// Get filtered nodes for a robot model based on VehicleType properties
|
||||
/// Returns only nodes that have NodeVehicleProperties for the robot model's VehicleType
|
||||
/// </summary>
|
||||
Task<List<Node>> GetFilteredNodesAsync(Guid robotModelId);
|
||||
|
||||
/// <summary>
|
||||
/// Get filtered edges for a robot model based on VehicleType properties
|
||||
/// Returns only edges that have EdgeVehicleProperties for the robot model's VehicleType
|
||||
/// </summary>
|
||||
Task<List<Edge>> GetFilteredEdgesAsync(Guid robotModelId);
|
||||
|
||||
/// <summary>
|
||||
/// Get filtered nodes for a robot model based on VehicleType properties and LevelId
|
||||
/// Returns only nodes that have NodeVehicleProperties for the robot model's VehicleType and belong to the specified level
|
||||
/// </summary>
|
||||
Task<List<Node>> GetFilteredNodesByLevelAsync(Guid robotModelId, Guid levelId);
|
||||
|
||||
/// <summary>
|
||||
/// Get filtered edges for a robot model based on VehicleType properties and LevelId
|
||||
/// Returns only edges that have EdgeVehicleProperties for the robot model's VehicleType and belong to the specified level
|
||||
/// </summary>
|
||||
Task<List<Edge>> GetFilteredEdgesByLevelAsync(Guid robotModelId, Guid levelId);
|
||||
|
||||
/// <summary>
|
||||
/// Get validated map data (nodes + edges) for a robot model
|
||||
/// Returns only valid nodes/edges after validation (nodes with edges, edges with both nodes)
|
||||
/// </summary>
|
||||
Task<RobotModelMapDataDto> GetValidatedMapDataAsync(Guid robotModelId);
|
||||
|
||||
/// <summary>
|
||||
/// Validate map data for a robot model
|
||||
/// Returns validation result with errors/warnings
|
||||
/// </summary>
|
||||
Task<MapValidationResultDto> ValidateMapForRobotModelAsync(Guid robotModelId);
|
||||
|
||||
/// <summary>
|
||||
/// Check if robot model has valid map configuration
|
||||
/// Returns true if VehicleTypeId and MapId are set and map data is valid
|
||||
/// </summary>
|
||||
Task<bool> HasValidMapConfigurationAsync(Guid robotModelId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.RobotModel;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing robot models
|
||||
/// </summary>
|
||||
public interface IRobotModelService
|
||||
{
|
||||
Task<RobotModel> CreateAsync(CreateRobotModelRequest request);
|
||||
Task<List<RobotModel>> GetAllAsync();
|
||||
Task<RobotModel?> GetByIdAsync(Guid id);
|
||||
Task<RobotModel> UpdateAsync(Guid id, UpdateRobotModelRequest request);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
Task<bool> ExistsAsync(string modelName);
|
||||
Task<List<RobotModel>> SearchAsync(string query);
|
||||
Task<RobotModelUsageInfoDto> GetUsageInfoAsync(Guid id);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Robot;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing robots
|
||||
/// </summary>
|
||||
public interface IRobotService
|
||||
{
|
||||
Task<Robot> CreateAsync(CreateRobotRequest request);
|
||||
Task<List<Robot>> GetAllAsync(Guid? modelId = null, Guid? mapId = null);
|
||||
Task<Robot?> GetByIdAsync(Guid id);
|
||||
Task<Robot?> GetByRobotIdAsync(string robotId);
|
||||
Task<Robot> UpdateAsync(Guid id, UpdateRobotRequest request);
|
||||
Task<bool> DeleteAsync(Guid id);
|
||||
Task<List<Robot>> GetByModelIdAsync(Guid modelId);
|
||||
Task<List<Robot>> GetByModelNameAsync(string modelName);
|
||||
Task<List<Robot>> SearchAsync(string query);
|
||||
Task<bool> ExistsAsync(string robotId);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
public class Logger<T>(ILogger<T> Logger) where T : class
|
||||
{
|
||||
public event Action? LoggerUpdate;
|
||||
public void Write(string message, LogLevel level)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel.Trace:
|
||||
if(Logger.IsEnabled(LogLevel.Trace))Logger.LogTrace("{mes}", message);
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
if (Logger.IsEnabled(LogLevel.Debug)) Logger.LogDebug("{mes}", message);
|
||||
break;
|
||||
case LogLevel.Information:
|
||||
if (Logger.IsEnabled(LogLevel.Information)) Logger.LogInformation("{mes}", message);
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
if (Logger.IsEnabled(LogLevel.Warning)) Logger.LogWarning("{mes}", message);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
if (Logger.IsEnabled(LogLevel.Error)) Logger.LogError("{mes}", message);
|
||||
break;
|
||||
case LogLevel.Critical:
|
||||
if (Logger.IsEnabled(LogLevel.Critical)) Logger.LogCritical("{mes}", message);
|
||||
break;
|
||||
}
|
||||
LoggerUpdate?.Invoke();
|
||||
}
|
||||
|
||||
public void Write(string message)
|
||||
{
|
||||
Write(message, LogLevel.Information);
|
||||
}
|
||||
|
||||
public async Task WriteAsync(string message)
|
||||
{
|
||||
var write = Task.Run(() => Write(message));
|
||||
await write.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task TraceAsync(string message)
|
||||
{
|
||||
var write = Task.Run(() => Write(message, LogLevel.Trace));
|
||||
await write.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task DebugAsync(string message)
|
||||
{
|
||||
var write = Task.Run(() => Write(message, LogLevel.Debug));
|
||||
await write.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task InfoAsync(string message)
|
||||
{
|
||||
var write = Task.Run(() => Write(message, LogLevel.Information));
|
||||
await write.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task WarningAsync(string message)
|
||||
{
|
||||
var write = Task.Run(() => Write(message, LogLevel.Warning));
|
||||
await write.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task ErrorAsync(string message)
|
||||
{
|
||||
var write = Task.Run(() => Write(message, LogLevel.Error));
|
||||
await write.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task CriticalAsync(string message)
|
||||
{
|
||||
var write = Task.Run(() => Write(message, LogLevel.Critical));
|
||||
await write.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public void Trace(string message)
|
||||
{
|
||||
Write(message, LogLevel.Trace);
|
||||
}
|
||||
|
||||
public void Debug(string message)
|
||||
{
|
||||
Write(message, LogLevel.Debug);
|
||||
}
|
||||
|
||||
public void Info(string message)
|
||||
{
|
||||
Write(message, LogLevel.Information);
|
||||
}
|
||||
|
||||
public void Warning(string message)
|
||||
{
|
||||
Write(message, LogLevel.Warning);
|
||||
}
|
||||
|
||||
public void Error(string message)
|
||||
{
|
||||
Write(message, LogLevel.Error);
|
||||
}
|
||||
|
||||
public void Critical(string message)
|
||||
{
|
||||
Write(message, LogLevel.Critical);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public class ACSHeader(string messageName, string time)
|
||||
{
|
||||
[JsonPropertyName("msgname")]
|
||||
[Required]
|
||||
public string MessageName { get; set; } = messageName;
|
||||
|
||||
[JsonPropertyName("time")]
|
||||
[Required]
|
||||
public string Time { get; set; } = time;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public class ACSPublishModel
|
||||
{
|
||||
[JsonPropertyName("header")]
|
||||
[Required]
|
||||
public ACSHeader Header { get; set; } = new("AGV_STATUS", DateTime.Today.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||
|
||||
[JsonPropertyName("body")]
|
||||
[Required]
|
||||
public RobotPublishStatusBody Body { get; set; } = new();
|
||||
}
|
||||
|
||||
public class RobotPublishStatusBody
|
||||
{
|
||||
[JsonPropertyName("agv_id")]
|
||||
[Required]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("state")]
|
||||
[Required]
|
||||
public string State { get; set; } = "-1";
|
||||
|
||||
[JsonPropertyName("site_code")]
|
||||
[Required]
|
||||
public string SiteCode { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("area_code")]
|
||||
[Required]
|
||||
public string AreaCode { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("area_name")]
|
||||
[Required]
|
||||
public string AreaName { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("location")]
|
||||
[Required]
|
||||
public AGVLocation Location { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("marker_id")]
|
||||
[Required]
|
||||
public string? MarkerId { get; set; }
|
||||
|
||||
[JsonPropertyName("battery_level")]
|
||||
[Required]
|
||||
public string BatteryLevel { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("battery_voltage")]
|
||||
[Required]
|
||||
public string BatteryVoltage { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("battery_current")]
|
||||
[Required]
|
||||
public string BatteryCurrent { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("battery_temperature")]
|
||||
[Required]
|
||||
public string BatteryTemprature { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("battery_id")]
|
||||
[Required]
|
||||
public string? BatteryId { get; set; }
|
||||
|
||||
[JsonPropertyName("battery_soh")]
|
||||
[Required]
|
||||
public string? BatterySOH { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("loading")]
|
||||
[Required]
|
||||
public string Loading { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("error_code")]
|
||||
[Required]
|
||||
public string? ErrorCode { get; set; }
|
||||
|
||||
[JsonPropertyName("station_id")]
|
||||
[Required]
|
||||
public string? StationId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public class ACSStatusBodyResponse
|
||||
{
|
||||
[JsonPropertyName("result")]
|
||||
[Required]
|
||||
public string Result { get; set; } = string.Empty;
|
||||
|
||||
}
|
||||
|
||||
public class ACSStatusResponse
|
||||
{
|
||||
[JsonPropertyName("header")]
|
||||
[Required]
|
||||
public ACSHeader? Header { get; set; }
|
||||
|
||||
|
||||
[JsonPropertyName("body")]
|
||||
[Required]
|
||||
public ACSStatusBodyResponse? Body { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public class AGVLocation
|
||||
{
|
||||
[JsonPropertyName("world_x")]
|
||||
[Required]
|
||||
public string X { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("world_y")]
|
||||
[Required]
|
||||
public string Y { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("world_z")]
|
||||
[Required]
|
||||
public string Z { get; set; } = "0";
|
||||
|
||||
[JsonPropertyName("direction")]
|
||||
[Required]
|
||||
public string Direction { get; set; } = "0";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public enum AGVState
|
||||
{
|
||||
Offline = -1,
|
||||
Error = 0,
|
||||
Idle = 1,
|
||||
Processing = 2,
|
||||
Pause = 3,
|
||||
DockingFail = 4,
|
||||
NoPose = 5,
|
||||
Charging = 6,
|
||||
Run = 7,
|
||||
Stop = 8,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public class OpenACSException : Exception
|
||||
{
|
||||
public OpenACSException() { }
|
||||
public OpenACSException(string message) : base(message) { }
|
||||
public OpenACSException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Type;
|
||||
using RobotNet10.Common;
|
||||
using RobotNet10.FleetManager.Services.ConfigManager;
|
||||
using RobotNet10.FleetManager.Services.RobotManager;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public class OpenACSPublisher(IConfiguration configuration,
|
||||
Logger<OpenACSPublisher> Logger,
|
||||
ILogger<OpenACSPublisher> ILogger,
|
||||
IRobotManagerService RobotManager,
|
||||
IACSTrafficConfig ACSTrafficConfig) : BackgroundService
|
||||
{
|
||||
public int PublishCount { get; private set; }
|
||||
private WatchTimerAsync<OpenACSPublisher>? Timer;
|
||||
private readonly string ACSSiteCode = configuration["ACSStatusConfig:SiteCode"] ?? "VN03";
|
||||
private readonly string ACSAreaCode = configuration["ACSStatusConfig:AreaCode"] ?? "DA3_FL1";
|
||||
private readonly string ACSAreaName = configuration["ACSStatusConfig:AreaName"] ?? "DA3_WM";
|
||||
private readonly double ACSExtendX = configuration.GetValue<double>("ACSStatusConfig:ExtendX");
|
||||
private readonly double ACSExtendY = configuration.GetValue<double>("ACSStatusConfig:ExtendY");
|
||||
private readonly double ACSExtendTheta = configuration.GetValue<double>("ACSStatusConfig:ExtendTheta");
|
||||
|
||||
private readonly SemaphoreSlim _timerLock = new(1, 1);
|
||||
private async Task TimerHandler()
|
||||
{
|
||||
if (ACSTrafficConfig.PublishEnable && !string.IsNullOrEmpty(ACSTrafficConfig.PublishURL))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var HttpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) };
|
||||
var robotControllers = RobotManager.GetAllRobotControllers();
|
||||
foreach (var robot in robotControllers)
|
||||
{
|
||||
var startTime = DateTime.Now;
|
||||
if (robot.Value.Data.State == null || robot.Value.Data.State.AgvPosition == null) continue;
|
||||
if ((startTime - robot.Value.Data.State.Timestamp).TotalMilliseconds > ACSTrafficConfig.PublishInterval) continue;
|
||||
|
||||
int batLevel = (int)robot.Value.Data.State.BatteryState.BatteryHealth;
|
||||
if (batLevel <= 0) batLevel = 85;
|
||||
|
||||
int batVol = (int)(robot.Value.Data.State.BatteryState.BatteryVoltage ?? 0);
|
||||
if (batVol <= 0) batVol = 24;
|
||||
var status = new ACSPublishModel()
|
||||
{
|
||||
Header = new("AGV_STATUS", robot.Value.Data.State.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff")),
|
||||
Body = new()
|
||||
{
|
||||
Id = robot.Key,
|
||||
Location = new()
|
||||
{
|
||||
X = (robot.Value.Data.State.AgvPosition.X + ACSExtendX).ToString(),
|
||||
Y = (robot.Value.Data.State.AgvPosition.Y + ACSExtendY).ToString(),
|
||||
Z = "0",
|
||||
Direction = (robot.Value.Data.State.AgvPosition.Theta + ACSExtendTheta).ToString(),
|
||||
},
|
||||
SiteCode = ACSSiteCode,
|
||||
AreaCode = ACSAreaCode,
|
||||
AreaName = ACSAreaName,
|
||||
MarkerId = string.IsNullOrEmpty(robot.Value.Data.State.LastNodeId) ? null : robot.Value.Data.State.LastNodeId,
|
||||
BatteryId = null,
|
||||
BatteryLevel = batLevel.ToString(),
|
||||
BatteryVoltage = batVol.ToString(),
|
||||
BatterySOH = null,
|
||||
BatteryCurrent = "1.0",
|
||||
BatteryTemprature = "30",
|
||||
StationId = null,
|
||||
Loading = robot.Value.Data.State.Loads.Length != 0 ? "1" : "0",
|
||||
ErrorCode = GetErrorCode(robot.Value.Data.State.Errors ?? [])?.ToString() ?? null,
|
||||
State = GetStatus(robot.Value.Data.State).ToString(),
|
||||
}
|
||||
};
|
||||
|
||||
var response = await HttpClient.PostAsJsonAsync(ACSTrafficConfig.PublishURL, status);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ACSStatusResponse>();
|
||||
if (result == null)
|
||||
{
|
||||
Logger.Error("Failed to convert response.Content to ACSStatusResponse");
|
||||
}
|
||||
else if (result.Header?.MessageName == "AGV_STATUS_ACK" && result.Body?.Result == "OK")
|
||||
{
|
||||
PublishCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning($"ACS response is not OK: {System.Text.Json.JsonSerializer.Serialize(result)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning($"ACS publish to {ACSTrafficConfig.PublishURL} failed: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"ACS publish to {ACSTrafficConfig.PublishURL} error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetStatus(StateMsg state)
|
||||
{
|
||||
if (GetError(state) == ErrorLevel.FATAL || GetError(state) == ErrorLevel.WARNING) return (int)AGVState.Error;
|
||||
else if (state.BatteryState.Charging) return (int)AGVState.Charging;
|
||||
else if (state.Paused) return (int)AGVState.Pause;
|
||||
else if (IsIdle(state)) return (int)AGVState.Idle;
|
||||
else if (IsWorking(state)) return (int)AGVState.Run;
|
||||
else return (int)AGVState.Stop;
|
||||
}
|
||||
|
||||
private static string? GetErrorCode(Error[] errors)
|
||||
{
|
||||
var error = errors.FirstOrDefault();
|
||||
if (error is not null && int.TryParse(error.ErrorType, out int errorCode)) return errorCode.ToString();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsIdle(StateMsg state)
|
||||
{
|
||||
if (state.NodeStates.Length != 0 || state.EdgeStates.Length != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsWorking(StateMsg state)
|
||||
{
|
||||
if (state.NodeStates.Length != 0 || state.EdgeStates.Length != 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ErrorLevel GetError(StateMsg state)
|
||||
{
|
||||
if (state.Errors is not null)
|
||||
{
|
||||
if (state.Errors.Any(error => error.ErrorLevel == ErrorLevel.FATAL)) return ErrorLevel.FATAL;
|
||||
if (state.Errors.Any(error => error.ErrorLevel == ErrorLevel.WARNING)) return ErrorLevel.WARNING;
|
||||
}
|
||||
return ErrorLevel.NONE;
|
||||
}
|
||||
|
||||
private async Task InitializeTimerAsync()
|
||||
{
|
||||
if (ACSTrafficConfig.PublishInterval == Timer?.Interval) return;
|
||||
if (_timerLock.Wait(1000))
|
||||
{
|
||||
try
|
||||
{
|
||||
Timer?.Dispose();
|
||||
Timer = new(ACSTrafficConfig.PublishInterval, TimerHandler, ILogger);
|
||||
Timer.Start();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_timerLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
ACSTrafficConfig.ConfigChanged += ConfigChanged;
|
||||
await InitializeTimerAsync();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"ACS Publisher: Initialization error: {ex.Message}");
|
||||
await Task.Delay(2000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void ConfigChanged(object? sender, EventArgs e)
|
||||
{
|
||||
await InitializeTimerAsync();
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ACSTrafficConfig.ConfigChanged -= ConfigChanged;
|
||||
Timer?.Dispose();
|
||||
_timerLock?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using RobotNet10.FleetManager.Services.ConfigManager;
|
||||
using RobotNet10.Shared;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public class TrafficACS(IACSTrafficConfig OpenACSManager, Logger<TrafficACS> Logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions jsonSerializeOptions = new() {
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
public async Task<MessageResult<bool>> RequestIn(string robotId, string zoneId)
|
||||
{
|
||||
var model = new TrafficACSRequest(robotId, zoneId, TrafficRequestType.IN);
|
||||
TrafficACSResponse? response = null;
|
||||
HttpResponseMessage? responseStr = null;
|
||||
try
|
||||
{
|
||||
if (!OpenACSManager.TrafficEnable) return new(true, true, "Kết nối với hệ thống traffic ACS không được bật");
|
||||
using var HttpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) };
|
||||
|
||||
responseStr = await HttpClient.PostAsJsonAsync(OpenACSManager.TrafficURL, model);
|
||||
response = await responseStr.Content.ReadFromJsonAsync<TrafficACSResponse>() ?? throw new OpenACSException("Lỗi giao tiếp với hệ thống traffic ACS");
|
||||
|
||||
if (response.AgvId != robotId) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS agv_id trả về {response.AgvId} không trùng với dữ liệu gửi đi {robotId}");
|
||||
if (response.TrafficZoneId != zoneId) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS traffic_zone_id trả về {response.TrafficZoneId} không trùng với dữ liệu gửi đi {zoneId}");
|
||||
if (response.InOut != TrafficRequestType.IN) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS inout trả về {response.InOut} không trùng với dữ liệu gửi đi in");
|
||||
if (response.Result != TrafficACSResult.GO) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS result trả về {response.Result} - không cho phép đi vào vùng {zoneId}");
|
||||
|
||||
Logger.Info($"{robotId} request into traffic zone {zoneId} succeeded");
|
||||
return new(true, true, "Request into traffic zone succeeded");
|
||||
}
|
||||
catch (OpenACSException ex)
|
||||
{
|
||||
Logger.Warning($"{robotId} request in error: {ex.Message}. \nRequest: {JsonSerializer.Serialize(model, jsonSerializeOptions)}\n Response: {(response is null ? "" : JsonSerializer.Serialize(response, jsonSerializeOptions))}, Raw: {(responseStr is null ? "" : await responseStr.Content.ReadAsStringAsync())}");
|
||||
return new(false, false, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"{robotId} request In error: {ex.Message} \nRaw: {(responseStr is null ? "" : await responseStr.Content.ReadAsStringAsync())}");
|
||||
return new(false, false, "Traffic ACS communication error");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MessageResult<bool>> RequestOut(string robotId, string zoneId)
|
||||
{
|
||||
var model = new TrafficACSRequest(robotId, zoneId, TrafficRequestType.OUT);
|
||||
TrafficACSResponse? response = null;
|
||||
try
|
||||
{
|
||||
if (!OpenACSManager.TrafficEnable) return new(true, true, "Kết nối với hệ thống traffic ACS không được bật");
|
||||
using var HttpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) };
|
||||
|
||||
response = await (await HttpClient.PostAsJsonAsync(OpenACSManager.TrafficURL, model)).Content.ReadFromJsonAsync<TrafficACSResponse>() ??
|
||||
throw new OpenACSException("Lỗi giao tiếp với hệ thống traffic ACS");
|
||||
|
||||
if (response.AgvId != robotId) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS agv_id trả về {response.AgvId} không trùng với dữ liệu gửi đi {robotId}");
|
||||
if (response.TrafficZoneId != zoneId) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS traffic_zone_id trả về {response.TrafficZoneId} không trùng với dữ liệu gửi đi {zoneId}");
|
||||
if (response.InOut != TrafficRequestType.OUT) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS inout trả về {response.InOut} không trùng với dữ liệu gửi đi out");
|
||||
if (response.Result != TrafficACSResult.GO) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS result trả về {response.Result} - không cho phép xóa bỏ vùng {zoneId}");
|
||||
|
||||
Logger.Info($"{robotId} request out of traffic zone {zoneId} succeeded");
|
||||
return new(true, true, "Request out of traffic zone succeeded");
|
||||
}
|
||||
catch (OpenACSException ex)
|
||||
{
|
||||
Logger.Warning($"{robotId} request out error: {ex.Message}. \nRequest: {JsonSerializer.Serialize(model, jsonSerializeOptions)}\n Response: {(response is null ? "" : JsonSerializer.Serialize(response, jsonSerializeOptions))}");
|
||||
return new(false, false, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"{robotId} request Out error: {ex.Message}");
|
||||
return new(false, false, "Traffic ACS communication error");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
|
||||
public class TrafficRequestType
|
||||
{
|
||||
public static string IN => "in";
|
||||
public static string OUT => "out";
|
||||
}
|
||||
|
||||
public class TrafficACSRequestBody(string agvId, string trafficZoneId, string inOut)
|
||||
{
|
||||
[JsonPropertyName("agvid")]
|
||||
[Required]
|
||||
public string AgvId { get; set; } = agvId;
|
||||
|
||||
[JsonPropertyName("area")]
|
||||
[Required]
|
||||
public string TrafficZoneId { get; set; } = trafficZoneId;
|
||||
|
||||
[JsonPropertyName("inout")]
|
||||
[Required]
|
||||
public string InOut { get; set; } = inOut;
|
||||
}
|
||||
|
||||
public class TrafficACSRequest
|
||||
{
|
||||
[JsonPropertyName("header")]
|
||||
[Required]
|
||||
public ACSHeader Header { get; set; } = new("TRAFFIC_REQ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||
|
||||
[JsonPropertyName("body")]
|
||||
[Required]
|
||||
public TrafficACSRequestBody Body { get; set; }
|
||||
|
||||
public TrafficACSRequest(string agvId, string trafficZoneId, string inOut)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(agvId))
|
||||
throw new ArgumentException("AGV ID không thể rỗng.", nameof(agvId));
|
||||
if (string.IsNullOrWhiteSpace(trafficZoneId))
|
||||
throw new ArgumentException("Traffic Zone ID không thể rỗng.", nameof(trafficZoneId));
|
||||
if (string.IsNullOrWhiteSpace(inOut))
|
||||
throw new ArgumentException("In OUT không thể rỗng.", nameof(inOut));
|
||||
|
||||
Body = new(agvId, trafficZoneId, inOut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.OpenACS;
|
||||
public class TrafficACSResult
|
||||
{
|
||||
public static string GO => "go";
|
||||
public static string NO => "no";
|
||||
}
|
||||
public class TrafficACSResponse
|
||||
{
|
||||
[JsonPropertyName("time")]
|
||||
[Required]
|
||||
public string? Time { get; set; }
|
||||
|
||||
[JsonPropertyName("agv_id")]
|
||||
[Required]
|
||||
public string? AgvId { get; set; }
|
||||
|
||||
[JsonPropertyName("traffic_zone_id")]
|
||||
[Required]
|
||||
public string? TrafficZoneId { get; set; }
|
||||
|
||||
[JsonPropertyName("inout")]
|
||||
[Required]
|
||||
public string? InOut { get; set; }
|
||||
|
||||
[JsonPropertyName("result")]
|
||||
[Required]
|
||||
public string? Result { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using RobotNet.VDA5050.InstantAction;
|
||||
using RobotNet.VDA5050.Order;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.RobotConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for managing MQTT connections to robots via VDA5050 protocol
|
||||
/// </summary>
|
||||
public interface IRobotConnectionsService
|
||||
{
|
||||
/// <summary>
|
||||
/// Start MQTT connection and subscribe to topics
|
||||
/// </summary>
|
||||
Task StartAsync(CancellationToken? cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Stop MQTT connection
|
||||
/// </summary>
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Check if MQTT client is connected
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Publish order message to a robot
|
||||
/// </summary>
|
||||
Task<bool> PublishOrderAsync(string robotId, OrderMsg order, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Publish instant actions message to a robot
|
||||
/// </summary>
|
||||
Task<bool> PublishInstantActionsAsync(string robotId, InstantActionsMsg instantActions, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace RobotNet10.FleetManager.Services.RobotConnections.Models;
|
||||
|
||||
/// <summary>
|
||||
/// VDA5050 Protocol configuration
|
||||
/// </summary>
|
||||
public class VDA5050ProtocolConfig
|
||||
{
|
||||
public string Manufacturer { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public string TopicPrefix { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using MQTTnet;
|
||||
using MQTTnet.Packets;
|
||||
using RobotNet.VDA5050;
|
||||
using RobotNet.VDA5050.Connection;
|
||||
using RobotNet.VDA5050.Factsheet;
|
||||
using RobotNet.VDA5050.InstantAction;
|
||||
using RobotNet.VDA5050.Order;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Visualization;
|
||||
using RobotNet10.FleetManager.Events;
|
||||
using RobotNet10.FleetManager.Services.ConfigManager;
|
||||
using RobotNet10.MqttConnection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.RobotConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing MQTT connections to robots via VDA5050 protocol
|
||||
/// </summary>
|
||||
public class RobotConnectionsService(
|
||||
IConnectionConfig configManager,
|
||||
IRobotEventBus eventBus,
|
||||
IServiceProvider serviceProvider,
|
||||
Logger<RobotConnectionsService> logger,
|
||||
ILogger<MQTTClient> mqttLogger) : IRobotConnectionsService
|
||||
{
|
||||
private readonly IConnectionConfig _configManager = configManager;
|
||||
private readonly IRobotEventBus _eventBus = eventBus;
|
||||
private readonly IServiceProvider _serviceProvider = serviceProvider;
|
||||
private readonly Logger<RobotConnectionsService> _logger = logger;
|
||||
private readonly ILogger<MQTTClient> _mqttLogger = mqttLogger;
|
||||
|
||||
private MQTTClient? _mqttClient;
|
||||
private readonly SemaphoreSlim _connectionSemaphore = new(1, 1);
|
||||
|
||||
public bool IsConnected => _mqttClient is not null && _mqttClient.IsConnected;
|
||||
|
||||
public async Task StartAsync(CancellationToken? cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await StopAsync();
|
||||
|
||||
if (!_connectionSemaphore.Wait(1000)) return;
|
||||
var mqttConfig = _configManager.GetMqttConfig();
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
|
||||
MqttTopicFilter[] topics = [
|
||||
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.STATE.ToJsonString()}")
|
||||
.WithAtMostOnceQoS()
|
||||
.Build(),
|
||||
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.VISUALIZATION.ToJsonString()}")
|
||||
.WithAtMostOnceQoS()
|
||||
.Build(),
|
||||
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.FACTSHEET.ToJsonString()}")
|
||||
.WithAtMostOnceQoS()
|
||||
.Build(),
|
||||
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.CONNECTION.ToJsonString()}")
|
||||
.WithAtMostOnceQoS()
|
||||
.Build()
|
||||
];
|
||||
|
||||
_mqttClient = new MQTTClient(mqttConfig, topics, _mqttLogger);
|
||||
_mqttClient.MessageUpdated += MessageUpdated;
|
||||
if (_mqttClient is not null) await _mqttClient.ConnectAsync(cancellationToken);
|
||||
if (_mqttClient is not null) await _mqttClient.SubscribeAsync(cancellationToken);
|
||||
|
||||
_logger.Info("RobotConnectionsService started successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Connection broker is failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (_mqttClient is not null)
|
||||
{
|
||||
await _mqttClient.DisposeAsync();
|
||||
_mqttClient = null;
|
||||
}
|
||||
_logger.Info("RobotConnectionsService stopped");
|
||||
}
|
||||
|
||||
private async Task MessageUpdated(MqttApplicationMessageReceivedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var topic = e.ApplicationMessage.Topic;
|
||||
var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||
var (robotId, messageType) = ParseVDA5050Topic(topic);
|
||||
if (!string.IsNullOrEmpty(robotId) && !string.IsNullOrEmpty(messageType))
|
||||
{
|
||||
using var scope = _serviceProvider.CreateAsyncScope();
|
||||
var _robotService = scope.ServiceProvider.GetRequiredService<IRobotService>();
|
||||
if (await _robotService.ExistsAsync(robotId))
|
||||
{
|
||||
if (messageType == VDA5050Topic.STATE.ToJsonString())
|
||||
{
|
||||
HandleStateMessageAsync(robotId, payload);
|
||||
}
|
||||
else if (messageType == VDA5050Topic.VISUALIZATION.ToJsonString())
|
||||
{
|
||||
HandleVisualizationMessageAsync(robotId, payload);
|
||||
}
|
||||
else if (messageType == VDA5050Topic.FACTSHEET.ToJsonString())
|
||||
{
|
||||
HandleFactsheetMessageAsync(robotId, payload);
|
||||
}
|
||||
else if (messageType == VDA5050Topic.CONNECTION.ToJsonString())
|
||||
{
|
||||
HandleConnectionMessageAsync(robotId, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning("Failed to parse topic");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Error processing message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private (string? robotId, string? messageType) ParseVDA5050Topic(string topic)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(topic)) return (null, null);
|
||||
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
ReadOnlySpan<char> topicSpan = topic.AsSpan();
|
||||
var manufacturerSpan = $"/{vdaConfig.Manufacturer}/".AsSpan();
|
||||
int manufacturerIndex = topicSpan.IndexOf(manufacturerSpan);
|
||||
|
||||
if (manufacturerIndex == -1) return (null, null);
|
||||
|
||||
var remaining = topicSpan[(manufacturerIndex + manufacturerSpan.Length)..];
|
||||
int firstSlash = remaining.IndexOf('/');
|
||||
if (firstSlash == -1) return (null, null);
|
||||
|
||||
var robotId = remaining[..firstSlash].ToString();
|
||||
var messageType = remaining[(firstSlash + 1)..].ToString();
|
||||
|
||||
return (robotId, messageType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Parse VDA5050 Topic failed: {ex}");
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PublishOrderAsync(string robotId, OrderMsg order, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_mqttClient is null)
|
||||
{
|
||||
_logger.Warning("Mqtt Client not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(robotId))
|
||||
{
|
||||
_logger.Warning("Cannot publish order: robotId is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (order == null)
|
||||
{
|
||||
_logger.Warning("Cannot publish order: order message is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(order.OrderId))
|
||||
{
|
||||
_logger.Warning("Cannot publish order: orderId is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.Warning($"Cannot publish order to robot {robotId}: MQTT client is not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var topic = BuildPublishTopic(robotId, VDA5050Topic.ORDER);
|
||||
var data = JsonSerializer.Serialize(order, JsonOptionExtends.Write);
|
||||
await _mqttClient.PublishAsync(topic, data);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Warning($"Publish order to robot {robotId} was cancelled");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error publishing order to robot {robotId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PublishInstantActionsAsync(string robotId, InstantActionsMsg instantActions, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_mqttClient is null)
|
||||
{
|
||||
_logger.Warning("Mqtt Client not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(robotId))
|
||||
{
|
||||
_logger.Warning("Cannot publish instantActions: robotId is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instantActions == null)
|
||||
{
|
||||
_logger.Warning("Cannot publish instantActions: instantActions message is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instantActions.Actions == null || instantActions.Actions.Length == 0)
|
||||
{
|
||||
_logger.Warning("Cannot publish instantActions: actions array is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.Warning($"Cannot publish instantActions to robot {robotId}: MQTT client is not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var topic = BuildPublishTopic(robotId, VDA5050Topic.INSTANTACTIONS);
|
||||
var data = JsonSerializer.Serialize(instantActions, JsonOptionExtends.Write);
|
||||
await _mqttClient.PublishAsync(topic, data);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Warning($"Publish instantActions to robot {robotId} was cancelled");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error publishing instantActions to robot {robotId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStateMessageAsync(string serialNumber, string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stateMsg = JsonSerializer.Deserialize<StateMsg>(payload, JsonOptionExtends.Read);
|
||||
if (stateMsg == null || stateMsg.SerialNumber != serialNumber) return;
|
||||
|
||||
_eventBus.PublishStateMessageReceived(serialNumber, stateMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error handling state message from robot {serialNumber}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionMessageAsync(string serialNumber, string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connectionMsg = JsonSerializer.Deserialize<ConnectionMsg>(payload, JsonOptionExtends.Read);
|
||||
if (connectionMsg == null || connectionMsg.SerialNumber != serialNumber) return;
|
||||
|
||||
_eventBus.PublishConnectionStateChanged(serialNumber, connectionMsg.ConnectionState);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error handling connection message from robot {serialNumber}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleVisualizationMessageAsync(string serialNumber, string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var visualizationMsg = JsonSerializer.Deserialize<VisualizationMsg>(payload, JsonOptionExtends.Read);
|
||||
if (visualizationMsg == null || visualizationMsg.SerialNumber != serialNumber) return;
|
||||
|
||||
_eventBus.PublishVisualizationMessageReceived(serialNumber, visualizationMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error handling visualization message from robot {serialNumber}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleFactsheetMessageAsync(string serialNumber, string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var factsheetMsg = JsonSerializer.Deserialize<FactSheetMsg>(payload, JsonOptionExtends.Read);
|
||||
if (factsheetMsg == null || factsheetMsg.SerialNumber != serialNumber) return;
|
||||
|
||||
_eventBus.PublishFactsheetMessageReceived(serialNumber, factsheetMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error handling factsheet message from robot {serialNumber}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildPublishTopic(string robotId, VDA5050Topic topic)
|
||||
{
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
return $"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/{robotId}/{topic.ToJsonString()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using RobotNet.VDA5050.InstantAction;
|
||||
using RobotNet.VDA5050.Order;
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Services.RobotManager.Models;
|
||||
using RobotNet10.Shared;
|
||||
using Action = RobotNet.VDA5050.InstantAction.Action;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.RobotController;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for RobotController - instance per robot
|
||||
/// </summary>
|
||||
public interface IRobotController : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Robot ID (SerialNumber)
|
||||
/// </summary>
|
||||
string RobotId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Robot data containing all robot information
|
||||
/// </summary>
|
||||
RobotData Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether robot is online (derived from ConnectionState)
|
||||
/// </summary>
|
||||
bool IsOnline { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the robot is ready for the order
|
||||
/// </summary>
|
||||
bool IsReady { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Move robot to a specific node by NodeName
|
||||
/// </summary>
|
||||
Task<MessageResult> MoveToNodeAsync(string nodeName, double? angle, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Move robot to a specific node by NodeId (string)
|
||||
/// </summary>
|
||||
Task<MessageResult> MoveToNodeByIdAsync(string nodeId, double? angle, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Move robot to a specific node by NodeId (Guid)
|
||||
/// </summary>
|
||||
Task<MessageResult> MoveToNodeByGuidAsync(Guid nodeId, double? angle, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Move the robot to a taget action and execute action
|
||||
/// </summary>
|
||||
/// <param name="nodeName"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<MessageResult> MoveToStationAsync(string nodeName, StationAction action, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Send a single instant action to robot
|
||||
/// </summary>
|
||||
Task<MessageResult> SendInstantActionAsync(Action action, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Send order to robot
|
||||
/// </summary>
|
||||
Task<MessageResult> SendOrderAsync(OrderMsg order, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Send instant actions message to robot
|
||||
/// </summary>
|
||||
Task<MessageResult> SendInstantActionsAsync(InstantActionsMsg instantActions, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Request factsheet from robot
|
||||
/// </summary>
|
||||
Task<MessageResult> RequestFactsheetAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Request state from robot
|
||||
/// </summary>
|
||||
Task<MessageResult> RequestStateAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel current order
|
||||
/// </summary>
|
||||
Task<MessageResult> CancelOrderAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
using RobotNet.VDA5050;
|
||||
using RobotNet.VDA5050.InstantAction;
|
||||
using RobotNet.VDA5050.Order;
|
||||
using RobotNet.VDA5050.Type;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Services.ConfigManager;
|
||||
using RobotNet10.FleetManager.Services.RobotConnections;
|
||||
using RobotNet10.FleetManager.Services.RobotManager.Models;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl.Models;
|
||||
using RobotNet10.MapManager.Services;
|
||||
using RobotNet10.Shared;
|
||||
using Action = RobotNet.VDA5050.InstantAction.Action;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.RobotController;
|
||||
|
||||
/// <summary>
|
||||
/// RobotController - instance per robot
|
||||
/// Mô hình hóa thông tin của 1 robot, định danh bằng RobotId (SerialNumber)
|
||||
/// </summary>
|
||||
public class RobotController : IRobotController
|
||||
{
|
||||
private readonly IRobotConnectionsService _robotConnectionsService;
|
||||
private readonly IConnectionConfig _configManager;
|
||||
private readonly ITrafficControlService _trafficControlService;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly Logger<RobotController> _logger;
|
||||
private readonly SemaphoreSlim _methodLock = new(1, 1);
|
||||
private readonly SemaphoreSlim _publishLock = new(1, 1);
|
||||
|
||||
private uint _headerIdCounter = 0;
|
||||
private readonly Lock _headerIdLock = new();
|
||||
|
||||
private readonly TimeSpan SendTimeOut = TimeSpan.FromSeconds(10);
|
||||
|
||||
public string RobotId { get; }
|
||||
public RobotData Data { get; }
|
||||
public bool IsOnline => Data.ConnectionState == ConnectionState.ONLINE;
|
||||
public bool IsReady => IsOnline && !IsRobotBusy();
|
||||
|
||||
public RobotController(
|
||||
string robotId,
|
||||
IRobotConnectionsService robotConnectionsService,
|
||||
IConnectionConfig configManager,
|
||||
ITrafficControlService trafficControlService,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
Logger<RobotController> logger)
|
||||
{
|
||||
RobotId = robotId;
|
||||
_robotConnectionsService = robotConnectionsService;
|
||||
_configManager = configManager;
|
||||
_trafficControlService = trafficControlService;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
|
||||
Data = new RobotData
|
||||
{
|
||||
RobotId = robotId,
|
||||
ConnectionState = ConnectionState.OFFLINE,
|
||||
LastUpdated = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<MessageResult> MoveToNodeAsync(string nodeName, double? angle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeName))
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: nodeName is null or empty for robot {RobotId}");
|
||||
return new(false, $"Cannot move to node: nodeName is null or empty for robot {RobotId}");
|
||||
}
|
||||
|
||||
await _methodLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// 1. Check if robot is busy with an order
|
||||
if (IsRobotBusy())
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: robot {RobotId} is busy with an order {Data.State?.OrderId}");
|
||||
return new(false, $"Robot {RobotId} is busy with an order");
|
||||
}
|
||||
|
||||
// 2. Get robot's levelId (MapId)
|
||||
var levelId = await GetRobotLevelIdAsync();
|
||||
if (levelId == null)
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: robot {RobotId} has no MapId assigned");
|
||||
return new(false, $"Robot {RobotId} has no MapId assigned");
|
||||
}
|
||||
|
||||
// 3. Find goal node by NodeName
|
||||
var goalNode = await FindNodeByNameAsync(levelId.Value, nodeName);
|
||||
if (goalNode == null)
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: node '{nodeName}' not found in level {levelId}");
|
||||
return new(false, $"Node '{nodeName}' not found");
|
||||
}
|
||||
|
||||
// 4. Plan route and send order
|
||||
return await PlanRouteAndSendOrderAsync(goalNode.Id, angle, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error in MoveToNodeAsync for robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error in MoveToNodeAsync for robot {RobotId}: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_methodLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move robot to a node by NodeId (string)
|
||||
/// </summary>
|
||||
public async Task<MessageResult> MoveToNodeByIdAsync(string nodeId, double? angle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: nodeId is null or empty for robot {RobotId}");
|
||||
return new(false, $"Cannot move to node: nodeId is null or empty for robot {RobotId}");
|
||||
}
|
||||
|
||||
await _methodLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// 1. Check if robot is busy with an order
|
||||
if (IsRobotBusy())
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: robot {RobotId} is busy with an order {Data.State?.OrderId}");
|
||||
return new(false, $"Robot {RobotId} is busy with an order");
|
||||
}
|
||||
|
||||
// 2. Get robot's levelId (MapId)
|
||||
var levelId = await GetRobotLevelIdAsync();
|
||||
if (levelId == null)
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: robot {RobotId} has no MapId assigned");
|
||||
return new(false, $"Robot {RobotId} has no MapId assigned");
|
||||
}
|
||||
|
||||
// 3. Find goal node by NodeId (string)
|
||||
var goalNode = await FindNodeByNodeIdAsync(levelId.Value, nodeId);
|
||||
if (goalNode == null)
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: node with NodeId '{nodeId}' not found in level {levelId}");
|
||||
return new(false, $"Node with NodeId '{nodeId}' not found");
|
||||
}
|
||||
|
||||
// 4. Plan route and send order
|
||||
return await PlanRouteAndSendOrderAsync(goalNode.Id, angle, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error in MoveToNodeByIdAsync for robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error in MoveToNodeByIdAsync for robot {RobotId}: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_methodLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move robot to a node by NodeId (Guid)
|
||||
/// </summary>
|
||||
public async Task<MessageResult> MoveToNodeByGuidAsync(Guid nodeId, double? angle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (nodeId == Guid.Empty)
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: nodeId is empty for robot {RobotId}");
|
||||
return new(false, $"Cannot move to node: nodeId is empty for robot {RobotId}");
|
||||
}
|
||||
|
||||
await _methodLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// 1. Check if robot is busy with an order
|
||||
if (IsRobotBusy())
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: robot {RobotId} is busy with an order {Data.State?.OrderId}");
|
||||
return new(false, $"Robot {RobotId} is busy with an order");
|
||||
}
|
||||
|
||||
// 2. Verify node exists
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var nodeService = scope.ServiceProvider.GetRequiredService<INodeService>();
|
||||
var goalNode = await nodeService.GetByIdAsync(nodeId);
|
||||
if (goalNode == null)
|
||||
{
|
||||
_logger.Warning($"Cannot move to node: node with Id '{nodeId}' not found");
|
||||
return new(false, $"Node with Id '{nodeId}' not found");
|
||||
}
|
||||
|
||||
// 3. Plan route and send order
|
||||
return await PlanRouteAndSendOrderAsync(nodeId, angle, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error in MoveToNodeByGuidAsync for robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error in MoveToNodeByGuidAsync for robot {RobotId}: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_methodLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<MessageResult> MoveToStationAsync(string nodeName, StationAction action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<MessageResult> SendInstantActionAsync(Action action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
_logger.Warning($"Cannot send instant action: action is null for robot {RobotId}");
|
||||
return new(false, $"Cannot send instant action: action is null for robot {RobotId}");
|
||||
}
|
||||
|
||||
await _methodLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var instantActions = new InstantActionsMsg
|
||||
{
|
||||
Actions = [action]
|
||||
};
|
||||
|
||||
FillVDA5050Header(instantActions);
|
||||
var publish = await _robotConnectionsService.PublishInstantActionsAsync(RobotId, instantActions, cancellationToken);
|
||||
return new(publish);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error sending instant action to robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error sending instant action to robot {RobotId}: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_methodLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MessageResult> SendOrderAsync(OrderMsg order, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (order == null)
|
||||
{
|
||||
_logger.Warning($"Cannot send order: order message is null for robot {RobotId}");
|
||||
return new(false, $"Cannot send order: order message is null for robot {RobotId}");
|
||||
}
|
||||
|
||||
await _publishLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// Fill header if not already set
|
||||
if (order.HeaderId == 0)
|
||||
{
|
||||
FillVDA5050Header(order);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure SerialNumber matches
|
||||
order.SerialNumber = RobotId;
|
||||
}
|
||||
|
||||
var published = await _robotConnectionsService.PublishOrderAsync(RobotId, order, cancellationToken);
|
||||
|
||||
// Update order in RobotData when successfully published
|
||||
if (published)
|
||||
{
|
||||
CancellationTokenSource cancelSend = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cancelSend.CancelAfter(SendTimeOut);
|
||||
while (true)
|
||||
{
|
||||
if (cancelSend.IsCancellationRequested) return new(false, "Publish order failed timout");
|
||||
if (Data.State is not null)
|
||||
{
|
||||
if (Data.State.OrderId == order.OrderId && IsRobotBusy() && Data.State.OrderUpdateId == order.OrderUpdateId)
|
||||
{
|
||||
Data.LastUpdated = DateTime.UtcNow;
|
||||
Data.Order = order;
|
||||
return new(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new(false, "Publish order failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error sending order to robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error sending order to robot {RobotId}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_publishLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MessageResult> SendInstantActionsAsync(InstantActionsMsg instantActions, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (instantActions == null)
|
||||
{
|
||||
_logger.Warning($"Cannot send instant actions: instantActions message is null for robot {RobotId}");
|
||||
return new(false, $"Cannot send instant actions: instantActions message is null for robot {RobotId}");
|
||||
}
|
||||
|
||||
if (instantActions.Actions == null || instantActions.Actions.Length == 0)
|
||||
{
|
||||
_logger.Warning($"Cannot send instant actions: actions array is null or empty for robot {RobotId}");
|
||||
return new(false, $"Cannot send instant actions: actions array is null or empty for robot {RobotId}");
|
||||
}
|
||||
|
||||
await _publishLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// Fill header if not already set
|
||||
if (instantActions.HeaderId == 0)
|
||||
{
|
||||
FillVDA5050Header(instantActions);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure SerialNumber matches
|
||||
instantActions.SerialNumber = RobotId;
|
||||
}
|
||||
|
||||
var publish = await _robotConnectionsService.PublishInstantActionsAsync(RobotId, instantActions, cancellationToken);
|
||||
return new(publish);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error sending instant actions to robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error sending instant actions to robot {RobotId}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_publishLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MessageResult> RequestFactsheetAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _methodLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var action = new Action
|
||||
{
|
||||
ActionType = ActionType.FACTSHEET_REQUEST.ToJsonString(),
|
||||
ActionId = Guid.NewGuid().ToString(),
|
||||
BlockingType = BlockingType.NONE
|
||||
};
|
||||
|
||||
return await SendInstantActionAsync(action, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error requesting factsheet from robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error requesting factsheet from robot {RobotId}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_methodLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MessageResult> RequestStateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _methodLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var action = new Action
|
||||
{
|
||||
ActionType = ActionType.STATE_REQUEST.ToJsonString(),
|
||||
ActionId = Guid.NewGuid().ToString(),
|
||||
BlockingType = BlockingType.NONE
|
||||
};
|
||||
|
||||
return await SendInstantActionAsync(action, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error requesting state from robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error requesting state from robot {RobotId}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_methodLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MessageResult> CancelOrderAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var action = new Action
|
||||
{
|
||||
ActionType = ActionType.CANCEL_ORDER.ToJsonString(),
|
||||
ActionId = Guid.NewGuid().ToString(),
|
||||
BlockingType = BlockingType.NONE,
|
||||
ActionDescription = "Cancel current order"
|
||||
};
|
||||
|
||||
var result = await SendInstantActionAsync(action, cancellationToken);
|
||||
if (result.IsSuccess && Data.State != null)
|
||||
{
|
||||
// Clear order state immediately so a new order can be accepted without waiting
|
||||
// for the robot to report empty NodeStates/EdgeStates (avoids "robot is busy" after cancel).
|
||||
Data.State.NodeStates = [];
|
||||
Data.State.EdgeStates = [];
|
||||
Data.State.OrderId = string.Empty;
|
||||
Data.State.OrderUpdateId = 0;
|
||||
Data.OrderClearedByCancelAt = DateTime.UtcNow;
|
||||
_logger.Info($"Robot {RobotId}: cleared order state after cancel (ready for new order)");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error canceling order for robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error canceling order for robot {RobotId}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if robot is busy with an order
|
||||
/// </summary>
|
||||
private bool IsRobotBusy()
|
||||
{
|
||||
// Robot is busy if order status is Sent or Accepted
|
||||
return Data.State?.NodeStates.Length > 0 || Data.State?.EdgeStates.Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot's levelId (MapId) from database
|
||||
/// </summary>
|
||||
private async Task<Guid?> GetRobotLevelIdAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var robotService = scope.ServiceProvider.GetRequiredService<IRobotService>();
|
||||
var robot = await robotService.GetByRobotIdAsync(RobotId);
|
||||
return robot?.MapId; // MapId is the levelId
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error getting levelId for robot {RobotId}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find node by NodeName in a level
|
||||
/// </summary>
|
||||
private async Task<RobotNet10.MapManager.Data.Node?> FindNodeByNameAsync(Guid levelId, string nodeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var nodeService = scope.ServiceProvider.GetRequiredService<INodeService>();
|
||||
|
||||
// Get all nodes in level
|
||||
var nodes = await nodeService.GetNodesByLevelAsync(levelId);
|
||||
|
||||
// Find by NodeName (case-insensitive)
|
||||
var node = nodes.FirstOrDefault(n =>
|
||||
!string.IsNullOrEmpty(n.NodeName) &&
|
||||
n.NodeName.Equals(nodeName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return node;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error finding node by name '{nodeName}' in level {levelId}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find node by NodeId (string) in a level
|
||||
/// </summary>
|
||||
private async Task<RobotNet10.MapManager.Data.Node?> FindNodeByNodeIdAsync(Guid levelId, string nodeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var nodeService = scope.ServiceProvider.GetRequiredService<INodeService>();
|
||||
|
||||
// Get all nodes in level
|
||||
var nodes = await nodeService.GetNodesByLevelAsync(levelId);
|
||||
|
||||
// Find by NodeId (case-insensitive)
|
||||
var node = nodes.FirstOrDefault(n =>
|
||||
n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return node;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error finding node by NodeId '{nodeId}' in level {levelId}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plan route and send order to robot
|
||||
/// </summary>
|
||||
private async Task<MessageResult> PlanRouteAndSendOrderAsync(Guid goalNodeId, double? angle, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if robot has state (required for route planning)
|
||||
if (Data.State == null || Data.State.AgvPosition == null)
|
||||
{
|
||||
_logger.Warning($"Cannot plan route: robot {RobotId} has no state or position");
|
||||
return new(false, $"Robot {RobotId} has no state or position");
|
||||
}
|
||||
|
||||
// Cancel current order if exists
|
||||
if (IsRobotBusy())
|
||||
{
|
||||
return new(false, $"The robot is working on order {Data.State.OrderId}");
|
||||
}
|
||||
|
||||
// Plan route using TrafficControlService from current position
|
||||
// Get current position from State.AgvPosition
|
||||
var currentX = Data.State.AgvPosition.X;
|
||||
var currentY = Data.State.AgvPosition.Y;
|
||||
var currentThetaRadians = Data.State.AgvPosition.Theta; // radians
|
||||
var currentThetaDegrees = currentThetaRadians * 180.0 / Math.PI; // convert to degrees
|
||||
|
||||
// Plan route from current position to goal node
|
||||
RobotRoute? route = await _trafficControlService.PlanRouteFromPositionACSTrafficAsync(
|
||||
RobotId,
|
||||
currentX,
|
||||
currentY,
|
||||
currentThetaDegrees,
|
||||
goalNodeId,
|
||||
angle, // goalAngle in degrees
|
||||
null, // startDirection
|
||||
null, // finalDirection
|
||||
cancellationToken);
|
||||
|
||||
if (route == null)
|
||||
{
|
||||
_logger.Warning($"Cannot plan route to node {goalNodeId} for robot {RobotId}");
|
||||
return new(false, $"Failed to plan route to node {goalNodeId}");
|
||||
}
|
||||
return new(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error planning route and sending order for robot {RobotId}: {ex.Message}");
|
||||
return new(false, $"Error planning route: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private uint GetNextHeaderId()
|
||||
{
|
||||
lock (_headerIdLock)
|
||||
{
|
||||
_headerIdCounter++;
|
||||
if (_headerIdCounter == 0) // Handle overflow
|
||||
{
|
||||
_headerIdCounter = 1;
|
||||
}
|
||||
return _headerIdCounter;
|
||||
}
|
||||
}
|
||||
|
||||
private void FillVDA5050Header(OrderMsg msg)
|
||||
{
|
||||
var config = _configManager.GetVDA5050Config();
|
||||
msg.HeaderId = GetNextHeaderId();
|
||||
msg.Timestamp = DateTime.UtcNow;
|
||||
msg.Version = config.Version;
|
||||
msg.Manufacturer = config.Manufacturer;
|
||||
msg.SerialNumber = RobotId;
|
||||
}
|
||||
|
||||
private void FillVDA5050Header(InstantActionsMsg msg)
|
||||
{
|
||||
var config = _configManager.GetVDA5050Config();
|
||||
msg.HeaderId = 1;
|
||||
msg.Timestamp = DateTime.UtcNow;
|
||||
msg.Version = config.Version;
|
||||
msg.Manufacturer = config.Manufacturer;
|
||||
msg.SerialNumber = RobotId;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup resources if needed
|
||||
_methodLock?.Dispose();
|
||||
// RobotData will be cleaned up by GC
|
||||
_logger.Debug($"RobotController disposed for robot {RobotId}");
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Services.RobotController;
|
||||
using RobotNet10.FleetManager.Services.RobotManager.Models;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.RobotManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing RobotController instances and routing events
|
||||
/// </summary>
|
||||
public interface IRobotManagerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Get RobotController instance for a robot
|
||||
/// </summary>
|
||||
IRobotController? GetRobotController(string robotId);
|
||||
|
||||
/// <summary>
|
||||
/// Get all RobotController instances
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, IRobotController> GetAllRobotControllers();
|
||||
|
||||
/// <summary>
|
||||
/// Remove RobotController instance (when robot is deleted from DB)
|
||||
/// </summary>
|
||||
void RemoveRobotController(string robotId);
|
||||
|
||||
/// <summary>
|
||||
/// Get RobotData for a robot (backward compatibility)
|
||||
/// </summary>
|
||||
RobotData? GetRobotData(string robotId);
|
||||
|
||||
/// <summary>
|
||||
/// Get all RobotData (backward compatibility)
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, RobotData> GetAllRobotData();
|
||||
|
||||
/// <summary>
|
||||
/// Get list of available (online) robots
|
||||
/// </summary>
|
||||
IReadOnlyList<string> GetAvailableRobots();
|
||||
|
||||
/// <summary>
|
||||
/// Get list of available robots with condititions
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<string>> GetAvailableRobots(string layout, string version, string level, string model, Func<RobotState, bool> func);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace RobotNet10.FleetManager.Services.RobotManager.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Order status enumeration
|
||||
/// </summary>
|
||||
//public enum OrderStatus
|
||||
//{
|
||||
// Pending, // Order đã tạo nhưng chưa gửi
|
||||
// Sent, // Order đã gửi qua MQTT
|
||||
// Accepted, // Robot đã accept order
|
||||
// Rejected, // Robot reject order
|
||||
// Completed, // Order hoàn thành
|
||||
// Failed // Order failed
|
||||
//}
|
||||
@@ -0,0 +1,55 @@
|
||||
using RobotNet.VDA5050.Connection;
|
||||
using RobotNet.VDA5050.Factsheet;
|
||||
using RobotNet.VDA5050.Order;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Type;
|
||||
using RobotNet.VDA5050.Visualization;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.RobotManager.Models;
|
||||
|
||||
/// <summary>
|
||||
/// RobotData chứa tất cả thông tin liên quan đến 1 robot
|
||||
/// </summary>
|
||||
public class RobotData
|
||||
{
|
||||
/// <summary>
|
||||
/// Robot ID (SerialNumber)
|
||||
/// </summary>
|
||||
public string RobotId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Latest State message
|
||||
/// </summary>
|
||||
public StateMsg? State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latest Connection state
|
||||
/// </summary>
|
||||
public ConnectionState ConnectionState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latest Order message
|
||||
/// </summary>
|
||||
public OrderMsg? Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latest Factsheet message
|
||||
/// </summary>
|
||||
public FactSheetMsg? Factsheet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latest Visualization message
|
||||
/// </summary>
|
||||
public VisualizationMsg? Visualization { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last updated timestamp
|
||||
/// </summary>
|
||||
public DateTime LastUpdated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When order state was cleared by cancel (so new order can be accepted before robot reports empty state).
|
||||
/// Used to avoid overwriting with stale state from robot; cleared when robot sends empty NodeStates/EdgeStates or after timeout.
|
||||
/// </summary>
|
||||
public DateTime? OrderClearedByCancelAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Type;
|
||||
using RobotNet10.Common;
|
||||
using RobotNet10.FleetManager.Controllers;
|
||||
using RobotNet10.FleetManager.Events;
|
||||
using RobotNet10.FleetManager.Events.Events;
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Services.ConfigManager;
|
||||
using RobotNet10.FleetManager.Services.RobotConnections;
|
||||
using RobotNet10.FleetManager.Services.RobotController;
|
||||
using RobotNet10.FleetManager.Services.RobotManager.Models;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl;
|
||||
using RobotNet10.MapManager.Services;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.RobotManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing RobotController instances and routing events
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This service:
|
||||
/// - Manages RobotController instances per robot (instance per robot)
|
||||
/// - Subscribes to Event Bus events and routes to RobotController
|
||||
/// - Auto-creates RobotController when receiving first Connection/State message
|
||||
/// - Timeout monitoring: 30s no State/Visualization → OFFLINE
|
||||
/// </remarks>
|
||||
public class RobotManagerService : BackgroundService, IRobotManagerService
|
||||
{
|
||||
private readonly IRobotEventBus _eventBus;
|
||||
private readonly IRobotConnectionsService _robotConnectionsService;
|
||||
private readonly IConnectionConfig _configManager;
|
||||
private readonly ITrafficControlService _trafficControlService;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly Logger<RobotManagerService> _logger;
|
||||
private readonly Logger<RobotController.RobotController> _loggerRobotController;
|
||||
|
||||
// RobotController instances - thread-safe dictionary
|
||||
private readonly ConcurrentDictionary<string, IRobotController> _robotControllers = new();
|
||||
|
||||
// Timeout tracking - last update time for State and Visualization per robot
|
||||
private readonly ConcurrentDictionary<string, (DateTime? lastStateUpdate, DateTime? lastVisualizationUpdate)> _lastUpdateTimes = new();
|
||||
|
||||
// Timeout monitoring timer
|
||||
private WatchTimerAsync<RobotManagerService>? _timeoutTimer;
|
||||
private const int TimeoutCheckIntervalMs = 15000; // 15 seconds
|
||||
private const int TimeoutThresholdSeconds = 30; // 30 seconds
|
||||
|
||||
public RobotManagerService(
|
||||
IRobotEventBus eventBus,
|
||||
IRobotConnectionsService robotConnectionsService,
|
||||
IConnectionConfig configManager,
|
||||
ITrafficControlService trafficControlService,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
Logger<RobotManagerService> logger,
|
||||
Logger<RobotController.RobotController> loggerRobotController)
|
||||
{
|
||||
_eventBus = eventBus;
|
||||
_robotConnectionsService = robotConnectionsService;
|
||||
_configManager = configManager;
|
||||
_trafficControlService = trafficControlService;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = logger;
|
||||
_loggerRobotController = loggerRobotController;
|
||||
|
||||
// Subscribe to Event Bus events
|
||||
_eventBus.StateMessageReceived += OnStateMessageReceived;
|
||||
_eventBus.ConnectionStateChanged += OnConnectionStateChanged;
|
||||
_eventBus.VisualizationMessageReceived += OnVisualizationMessageReceived;
|
||||
_eventBus.FactsheetMessageReceived += OnFactsheetMessageReceived;
|
||||
}
|
||||
|
||||
public IRobotController? GetRobotController(string robotId)
|
||||
{
|
||||
_robotControllers.TryGetValue(robotId, out var controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, IRobotController> GetAllRobotControllers()
|
||||
{
|
||||
return _robotControllers;
|
||||
}
|
||||
|
||||
public void RemoveRobotController(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_robotControllers.TryRemove(robotId, out var controller))
|
||||
{
|
||||
controller.Dispose();
|
||||
_lastUpdateTimes.TryRemove(robotId, out _);
|
||||
_logger.Info($"Removed RobotController for robot {robotId}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error removing RobotController for robot {robotId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public RobotData? GetRobotData(string robotId)
|
||||
{
|
||||
var controller = GetRobotController(robotId);
|
||||
return controller?.Data;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, RobotData> GetAllRobotData()
|
||||
{
|
||||
return _robotControllers.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.Data
|
||||
);
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetAvailableRobots()
|
||||
{
|
||||
return [.. _robotControllers
|
||||
.Where(kvp => kvp.Value.IsOnline)
|
||||
.Select(kvp => kvp.Key)];
|
||||
}
|
||||
|
||||
private static readonly TimeSpan OrderClearedByCancelWindow = TimeSpan.FromSeconds(15);
|
||||
|
||||
private void OnStateMessageReceived(object? sender, StateMessageReceivedEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robotId = e.RobotId;
|
||||
var stateMsg = e.StateMessage;
|
||||
|
||||
// Get or create RobotController
|
||||
var controller = GetOrCreateRobotController(robotId);
|
||||
if (controller == null) return;
|
||||
|
||||
// Update State in RobotData
|
||||
controller.Data.State = stateMsg;
|
||||
controller.Data.LastUpdated = DateTime.UtcNow;
|
||||
|
||||
var hasOrderState = (stateMsg.NodeStates?.Length ?? 0) > 0 || (stateMsg.EdgeStates?.Length ?? 0) > 0;
|
||||
var cancelOrderTimedOut = stateMsg.ActionStates?.Any(a =>
|
||||
(string.Equals(a.ActionType, "CANCEL_ORDER", StringComparison.OrdinalIgnoreCase) || a.ActionType?.Contains("cancelOrder", StringComparison.OrdinalIgnoreCase) == true) &&
|
||||
a.ActionStatus == ActionStatus.FAILED &&
|
||||
(a.ResultDescription?.Contains("Timeout", StringComparison.OrdinalIgnoreCase) ?? false)) == true;
|
||||
|
||||
// Force-clear order state when cancel was requested but timed out on robot (so FleetManager still shows robot as not busy)
|
||||
if (hasOrderState && cancelOrderTimedOut)
|
||||
{
|
||||
controller.Data.State.NodeStates = [];
|
||||
controller.Data.State.EdgeStates = [];
|
||||
controller.Data.State.OrderId = string.Empty;
|
||||
controller.Data.State.OrderUpdateId = 0;
|
||||
}
|
||||
// After cancel we clear order state locally; don't let stale robot state overwrite it until robot reports empty or window expires
|
||||
else
|
||||
{
|
||||
var clearedAt = controller.Data.OrderClearedByCancelAt;
|
||||
if (clearedAt.HasValue)
|
||||
{
|
||||
var elapsed = DateTime.UtcNow - clearedAt.Value;
|
||||
if (elapsed >= OrderClearedByCancelWindow)
|
||||
{
|
||||
controller.Data.OrderClearedByCancelAt = null;
|
||||
}
|
||||
else if (hasOrderState)
|
||||
{
|
||||
// Robot hasn't reported empty yet; keep order state cleared so new order can be accepted
|
||||
controller.Data.State.NodeStates = [];
|
||||
controller.Data.State.EdgeStates = [];
|
||||
controller.Data.State.OrderId = string.Empty;
|
||||
controller.Data.State.OrderUpdateId = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
controller.Data.OrderClearedByCancelAt = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update last State update time
|
||||
if(controller.Data.ConnectionState != RobotNet.VDA5050.Type.ConnectionState.ONLINE) controller.Data.ConnectionState = RobotNet.VDA5050.Type.ConnectionState.ONLINE;
|
||||
UpdateLastStateTime(robotId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error processing state message for robot {e.RobotId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConnectionStateChanged(object? sender, ConnectionStateChangedEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robotId = e.RobotId;
|
||||
var connectionState = e.ConnectionState;
|
||||
|
||||
// Get or create RobotController
|
||||
var controller = GetOrCreateRobotController(robotId);
|
||||
if (controller == null) return;
|
||||
|
||||
// Update Connection State in RobotData
|
||||
controller.Data.ConnectionState = connectionState;
|
||||
controller.Data.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error processing connection state change for robot {e.RobotId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVisualizationMessageReceived(object? sender, VisualizationMessageReceivedEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robotId = e.RobotId;
|
||||
var visualizationMsg = e.VisualizationMessage;
|
||||
|
||||
// Get or create RobotController
|
||||
var controller = GetOrCreateRobotController(robotId);
|
||||
if (controller == null) return;
|
||||
|
||||
// Update Visualization in RobotData
|
||||
controller.Data.Visualization = visualizationMsg;
|
||||
controller.Data.LastUpdated = DateTime.UtcNow;
|
||||
|
||||
// Update last Visualization update time
|
||||
UpdateLastVisualizationTime(robotId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error processing visualization message for robot {e.RobotId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFactsheetMessageReceived(object? sender, FactsheetMessageReceivedEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robotId = e.RobotId;
|
||||
var factsheetMsg = e.FactsheetMessage;
|
||||
|
||||
// Get or create RobotController
|
||||
var controller = GetOrCreateRobotController(robotId);
|
||||
if (controller == null) return;
|
||||
|
||||
// Update Factsheet in RobotData
|
||||
controller.Data.Factsheet = factsheetMsg;
|
||||
controller.Data.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error processing factsheet message for robot {e.RobotId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private IRobotController? GetOrCreateRobotController(string robotId)
|
||||
{
|
||||
// Check if already exists
|
||||
if (_robotControllers.TryGetValue(robotId, out var existingController))
|
||||
{
|
||||
return existingController;
|
||||
}
|
||||
|
||||
// Validate robot exists in database
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _robotService = scope.ServiceProvider.GetRequiredService<IRobotService>();
|
||||
if (!_robotService.ExistsAsync(robotId).Result)
|
||||
{
|
||||
_logger.Warning($"Robot {robotId} not found in database, skipping RobotController creation");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create new RobotController instance
|
||||
try
|
||||
{
|
||||
var controller = new Services.RobotController.RobotController(
|
||||
robotId,
|
||||
_robotConnectionsService,
|
||||
_configManager,
|
||||
_trafficControlService,
|
||||
_serviceScopeFactory,
|
||||
_loggerRobotController
|
||||
);
|
||||
|
||||
if (_robotControllers.TryAdd(robotId, controller))
|
||||
{
|
||||
_lastUpdateTimes.TryAdd(robotId, (null, null));
|
||||
_logger.Info($"Created RobotController for robot {robotId}");
|
||||
return controller;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Another thread created it, dispose this one and get the existing
|
||||
((IDisposable)controller).Dispose();
|
||||
_robotControllers.TryGetValue(robotId, out var createdController);
|
||||
return createdController;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error creating RobotController for robot {robotId}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<string>> GetAvailableRobots(string layout, string version, string level, string model, Func<RobotState, bool> func)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _robotService = scope.ServiceProvider.GetRequiredService<IRobotService>();
|
||||
var _layoutService = scope.ServiceProvider.GetRequiredService<ILayoutService>();
|
||||
|
||||
var layoutDb = await _layoutService.GetLayoutByNameAsync(layout);
|
||||
if (layoutDb is null) return [];
|
||||
var levelDb = layoutDb.Versions.FirstOrDefault(v => v.Version == version)?.Levels.FirstOrDefault(l => l.LayoutLevelId == level);
|
||||
if (levelDb is null) return [];
|
||||
|
||||
var robotDbs = await _robotService.GetByModelNameAsync(model);
|
||||
if (robotDbs is null) return [];
|
||||
|
||||
var robotDbinMap = robotDbs.Where(r => r.MapId == levelDb.Id);
|
||||
List<IRobotController> robotControllers = [.. _robotControllers.Where(kvp => kvp.Value.IsOnline && robotDbinMap.Any(r => r.RobotId == kvp.Key)).Select(kvp => kvp.Value)];
|
||||
return [..robotControllers.Where(r => r.IsOnline && r.Data is not null && r.Data.State is not null && r.Data.Visualization is not null && func(ToRobotState(r))).Select(r => r.RobotId)];
|
||||
}
|
||||
|
||||
private static RobotState ToRobotState(IRobotController robotController)
|
||||
{
|
||||
return new RobotState(robotController.IsReady,
|
||||
robotController.Data.State?.BatteryState.BatteryVoltage ?? 0,
|
||||
robotController.Data.State?.Loads ?? [],
|
||||
robotController.Data.State?.BatteryState.Charging ?? false,
|
||||
robotController.Data.Visualization?.AgvPosition.X ?? 0,
|
||||
robotController.Data.Visualization?.AgvPosition.Y ?? 0,
|
||||
robotController.Data.Visualization?.AgvPosition.Theta ?? 0);
|
||||
}
|
||||
|
||||
private void UpdateLastStateTime(string robotId)
|
||||
{
|
||||
_lastUpdateTimes.AddOrUpdate(robotId,
|
||||
(DateTime.UtcNow, null),
|
||||
(key, oldValue) => (DateTime.UtcNow, oldValue.lastVisualizationUpdate));
|
||||
}
|
||||
|
||||
private void UpdateLastVisualizationTime(string robotId)
|
||||
{
|
||||
_lastUpdateTimes.AddOrUpdate(robotId,
|
||||
(null, DateTime.UtcNow),
|
||||
(key, oldValue) => (oldValue.lastStateUpdate, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
private async Task CheckTimeouts()
|
||||
{
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var robotsToCheck = _robotControllers.Keys.ToList();
|
||||
|
||||
foreach (var robotId in robotsToCheck)
|
||||
{
|
||||
if (!_lastUpdateTimes.TryGetValue(robotId, out var updateTimes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var stateTimeout = updateTimes.lastStateUpdate.HasValue &&
|
||||
(now - updateTimes.lastStateUpdate.Value).TotalSeconds > TimeoutThresholdSeconds;
|
||||
|
||||
var visualizationTimeout = updateTimes.lastVisualizationUpdate.HasValue &&
|
||||
(now - updateTimes.lastVisualizationUpdate.Value).TotalSeconds > TimeoutThresholdSeconds;
|
||||
|
||||
// If both State and Visualization timeout, set OFFLINE
|
||||
if (stateTimeout && visualizationTimeout)
|
||||
{
|
||||
if (_robotControllers.TryGetValue(robotId, out var controller))
|
||||
{
|
||||
if (controller.Data.ConnectionState != RobotNet.VDA5050.Type.ConnectionState.OFFLINE)
|
||||
{
|
||||
controller.Data.ConnectionState = RobotNet.VDA5050.Type.ConnectionState.OFFLINE;
|
||||
controller.Data.LastUpdated = now;
|
||||
_logger.Warning($"Robot {robotId} timed out (30s no State/Visualization), set to OFFLINE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error in timeout check: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
await _robotConnectionsService.StartAsync(stoppingToken);
|
||||
|
||||
_timeoutTimer = new WatchTimerAsync<RobotManagerService>(
|
||||
TimeoutCheckIntervalMs,
|
||||
CheckTimeouts,
|
||||
_loggerFactory.CreateLogger<RobotManagerService>()
|
||||
);
|
||||
_timeoutTimer.Start();
|
||||
_logger.Info("Started timeout monitoring (15s interval, 30s threshold)");
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_timeoutTimer?.Dispose();
|
||||
|
||||
// Dispose all RobotController instances
|
||||
foreach (var controller in _robotControllers.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
controller.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error disposing RobotController: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
_robotControllers.Clear();
|
||||
_lastUpdateTimes.Clear();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RobotNet10.StorageManager;
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for robot model image storage using StorageManager
|
||||
/// Stores images with naming: {robotModelId}.png
|
||||
/// </summary>
|
||||
public class RobotModelImageStorageService : IRobotModelImageStorageService, IDisposable
|
||||
{
|
||||
private readonly ILogger<RobotModelImageStorageService> _logger;
|
||||
private readonly StorageManager.StorageManager _storageManager;
|
||||
private const string ImagePath = "robotModelImages";
|
||||
private const string ContentType = "image/png";
|
||||
|
||||
public RobotModelImageStorageService(IOptionsMonitor<StorageConfig> optionsSnapshot, ILogger<RobotModelImageStorageService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
var config = optionsSnapshot.Get("RobotModelImages");
|
||||
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
_storageManager = new StorageManager.StorageManager(config);
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
_logger.LogInformation("RobotModelImageStorageService initialized");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetObjectName(Guid robotModelId) => robotModelId.ToString();
|
||||
|
||||
public async Task SaveImageAsync(Guid robotModelId, Stream imageStream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var objectName = GetObjectName(robotModelId);
|
||||
|
||||
try
|
||||
{
|
||||
// Reset stream position if seekable
|
||||
if (imageStream.CanSeek)
|
||||
{
|
||||
imageStream.Position = 0;
|
||||
}
|
||||
|
||||
// Get stream size - handle cases where Length might not be available
|
||||
long size = imageStream.Length;
|
||||
|
||||
// If size is 0 or stream doesn't support Length, copy to MemoryStream
|
||||
if (size == 0 || !imageStream.CanSeek)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
await imageStream.CopyToAsync(memoryStream, cancellationToken);
|
||||
size = memoryStream.Length;
|
||||
memoryStream.Position = 0;
|
||||
|
||||
await _storageManager.UploadAsync(ImagePath, objectName, memoryStream, size, ContentType, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stream has valid length and is seekable, use directly
|
||||
await _storageManager.UploadAsync(ImagePath, objectName, imageStream, size, ContentType, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Failed to save image for robot model {RobotModelId}", robotModelId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Stream?> GetImageAsync(Guid robotModelId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var objectName = GetObjectName(robotModelId);
|
||||
|
||||
try
|
||||
{
|
||||
var exists = await _storageManager.ExistsAsync(ImagePath, objectName, cancellationToken);
|
||||
if (!exists)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for robot model {RobotModelId}", robotModelId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var stream = await _storageManager.GetFileAsync(ImagePath, objectName, cancellationToken);
|
||||
return stream;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for robot model {RobotModelId}", robotModelId);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Failed to read image for robot model {RobotModelId}", robotModelId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteImageAsync(Guid robotModelId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var objectName = GetObjectName(robotModelId);
|
||||
|
||||
try
|
||||
{
|
||||
var exists = await _storageManager.ExistsAsync(ImagePath, objectName, cancellationToken);
|
||||
if (!exists)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for deletion: {RobotModelId}", robotModelId);
|
||||
return false;
|
||||
}
|
||||
|
||||
await _storageManager.DeleteAsync(ImagePath, objectName, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Failed to delete image for robot model {RobotModelId}", robotModelId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ImageExistsAsync(Guid robotModelId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var objectName = GetObjectName(robotModelId);
|
||||
return await _storageManager.ExistsAsync(ImagePath, objectName, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<(int width, int height)> GetImageDimensionsAsync(Stream imageStream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Reset stream position if seekable
|
||||
if (imageStream.CanSeek)
|
||||
{
|
||||
imageStream.Position = 0;
|
||||
}
|
||||
|
||||
// Load image to get dimensions
|
||||
using var image = await Image.LoadAsync(imageStream, cancellationToken);
|
||||
|
||||
return (image.Width, image.Height);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to extract image dimensions");
|
||||
throw new InvalidOperationException("Invalid image format or corrupted file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_storageManager?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
using RobotNet10.FleetManager.Shared.DTOs.RobotModel;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Edge;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Node;
|
||||
using RobotNet10.MapManager.Data;
|
||||
using RobotNet10.MapManager.Services;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing robot model map data based on VehicleType filtering
|
||||
/// </summary>
|
||||
public class RobotModelMapService(
|
||||
IRobotModelService robotModelService,
|
||||
IVehicleTypeService vehicleTypeService,
|
||||
IMapQueryService mapQueryService,
|
||||
ILayoutService layoutService,
|
||||
Logger<RobotModelMapService> logger) : IRobotModelMapService
|
||||
{
|
||||
private readonly IRobotModelService _robotModelService = robotModelService;
|
||||
private readonly IVehicleTypeService _vehicleTypeService = vehicleTypeService;
|
||||
private readonly IMapQueryService _mapQueryService = mapQueryService;
|
||||
private readonly ILayoutService _layoutService = layoutService;
|
||||
private readonly Logger<RobotModelMapService> _logger = logger;
|
||||
|
||||
public async Task<List<Node>> GetFilteredNodesAsync(Guid robotModelId)
|
||||
{
|
||||
// Get RobotModel
|
||||
var robotModel = await _robotModelService.GetByIdAsync(robotModelId)
|
||||
?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found.");
|
||||
|
||||
// Validate VehicleTypeId is set
|
||||
if (!robotModel.VehicleTypeId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set.");
|
||||
}
|
||||
|
||||
var vehicleTypeId = robotModel.VehicleTypeId.Value;
|
||||
|
||||
// Validate VehicleType exists
|
||||
var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId)
|
||||
?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found.");
|
||||
|
||||
if (!vehicleType.IsActive)
|
||||
{
|
||||
_logger.Warning($"VehicleType '{vehicleType.VehicleTypeName}' is not active.");
|
||||
}
|
||||
|
||||
// Get filtered nodes: nodes that have NodeVehicleProperties for this VehicleType
|
||||
var filteredNodes = await _mapQueryService.GetNodesByVehicleTypeAsync(vehicleTypeId);
|
||||
|
||||
_logger.Info($"Found {filteredNodes.Count} filtered nodes for RobotModel '{robotModel.ModelName}' (VehicleType: '{vehicleType.VehicleTypeName}')");
|
||||
|
||||
return filteredNodes;
|
||||
}
|
||||
|
||||
public async Task<List<Edge>> GetFilteredEdgesAsync(Guid robotModelId)
|
||||
{
|
||||
// Get RobotModel
|
||||
var robotModel = await _robotModelService.GetByIdAsync(robotModelId)
|
||||
?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found.");
|
||||
|
||||
// Validate VehicleTypeId is set
|
||||
if (!robotModel.VehicleTypeId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set.");
|
||||
}
|
||||
|
||||
var vehicleTypeId = robotModel.VehicleTypeId.Value;
|
||||
|
||||
// Validate VehicleType exists
|
||||
var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId)
|
||||
?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found.");
|
||||
|
||||
// Get filtered edges: edges that have EdgeVehicleProperties for this VehicleType
|
||||
var filteredEdges = await _mapQueryService.GetEdgesByVehicleTypeAsync(vehicleTypeId);
|
||||
|
||||
_logger.Info($"Found {filteredEdges.Count} filtered edges for RobotModel '{robotModel.ModelName}' (VehicleType: '{vehicleType.VehicleTypeName}')");
|
||||
|
||||
return filteredEdges;
|
||||
}
|
||||
|
||||
public async Task<List<Node>> GetFilteredNodesByLevelAsync(Guid robotModelId, Guid levelId)
|
||||
{
|
||||
// Get RobotModel
|
||||
var robotModel = await _robotModelService.GetByIdAsync(robotModelId)
|
||||
?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found.");
|
||||
|
||||
// Validate VehicleTypeId is set
|
||||
if (!robotModel.VehicleTypeId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set.");
|
||||
}
|
||||
|
||||
var vehicleTypeId = robotModel.VehicleTypeId.Value;
|
||||
|
||||
// Validate VehicleType exists
|
||||
var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId)
|
||||
?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found.");
|
||||
|
||||
if (!vehicleType.IsActive)
|
||||
{
|
||||
_logger.Warning($"VehicleType '{vehicleType.VehicleTypeName}' is not active.");
|
||||
}
|
||||
|
||||
// Get filtered nodes: nodes that have NodeVehicleProperties for this VehicleType and belong to the specified level
|
||||
var filteredNodes = await _mapQueryService.GetNodesByVehicleTypeAndLevelAsync(vehicleTypeId, levelId);
|
||||
|
||||
_logger.Info($"Found {filteredNodes.Count} filtered nodes for RobotModel '{robotModel.ModelName}' (VehicleType: '{vehicleType.VehicleTypeName}') in LevelId={levelId}");
|
||||
|
||||
return filteredNodes;
|
||||
}
|
||||
|
||||
public async Task<List<Edge>> GetFilteredEdgesByLevelAsync(Guid robotModelId, Guid levelId)
|
||||
{
|
||||
// Get RobotModel
|
||||
var robotModel = await _robotModelService.GetByIdAsync(robotModelId)
|
||||
?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found.");
|
||||
|
||||
// Validate VehicleTypeId is set
|
||||
if (!robotModel.VehicleTypeId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set.");
|
||||
}
|
||||
|
||||
var vehicleTypeId = robotModel.VehicleTypeId.Value;
|
||||
|
||||
// Validate VehicleType exists
|
||||
var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId)
|
||||
?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found.");
|
||||
|
||||
// Get filtered edges: edges that have EdgeVehicleProperties for this VehicleType and belong to the specified level
|
||||
var filteredEdges = await _mapQueryService.GetEdgesByVehicleTypeAndLevelAsync(vehicleTypeId, levelId);
|
||||
|
||||
_logger.Info($"Found {filteredEdges.Count} filtered edges for RobotModel '{robotModel.ModelName}' (VehicleType: '{vehicleType.VehicleTypeName}') in LevelId={levelId}");
|
||||
|
||||
return filteredEdges;
|
||||
}
|
||||
|
||||
public async Task<RobotModelMapDataDto> GetValidatedMapDataAsync(Guid robotModelId)
|
||||
{
|
||||
// Get RobotModel
|
||||
var robotModel = await _robotModelService.GetByIdAsync(robotModelId)
|
||||
?? throw new KeyNotFoundException($"RobotModel with ID {robotModelId} not found.");
|
||||
|
||||
// Validate VehicleTypeId is set
|
||||
if (!robotModel.VehicleTypeId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException($"RobotModel '{robotModel.ModelName}' does not have VehicleTypeId set.");
|
||||
}
|
||||
|
||||
var vehicleTypeId = robotModel.VehicleTypeId.Value;
|
||||
|
||||
// Get VehicleType
|
||||
var vehicleType = await _vehicleTypeService.GetByIdAsync(vehicleTypeId)
|
||||
?? throw new KeyNotFoundException($"VehicleType with ID {vehicleTypeId} not found.");
|
||||
|
||||
// Get filtered nodes and edges (from all levels that match VehicleType)
|
||||
// Note: If RobotModel needs to filter by specific LevelId, MapId should be added to RobotModel
|
||||
var filteredNodes = await GetFilteredNodesAsync(robotModelId);
|
||||
var filteredEdges = await GetFilteredEdgesAsync(robotModelId);
|
||||
|
||||
// Get total counts across all levels (for reference)
|
||||
// In the future, if RobotModel has MapId (LevelId), we can filter by specific level
|
||||
int totalNodesInLevel = await _mapQueryService.GetTotalNodesCountAsync();
|
||||
int totalEdgesInLevel = await _mapQueryService.GetTotalEdgesCountAsync();
|
||||
string? levelName = null;
|
||||
Guid? levelId = null;
|
||||
|
||||
// If we have filtered nodes, get the level from first node (for display)
|
||||
if (filteredNodes.Count > 0)
|
||||
{
|
||||
var firstNode = filteredNodes.First();
|
||||
var level = await _layoutService.GetLevelAsync(firstNode.LevelId);
|
||||
levelName = level?.LayoutLevelId;
|
||||
levelId = level?.Id;
|
||||
}
|
||||
|
||||
// Validate: Remove nodes without edges, edges without both nodes
|
||||
var nodeIds = new HashSet<Guid>(filteredNodes.Select(n => n.Id));
|
||||
var validNodeIds = new HashSet<Guid>();
|
||||
var validEdges = new List<Edge>();
|
||||
|
||||
// First pass: Find edges that have both start and end nodes in filtered nodes
|
||||
foreach (var edge in filteredEdges)
|
||||
{
|
||||
if (nodeIds.Contains(edge.StartNodeId) && nodeIds.Contains(edge.EndNodeId))
|
||||
{
|
||||
validEdges.Add(edge);
|
||||
validNodeIds.Add(edge.StartNodeId);
|
||||
validNodeIds.Add(edge.EndNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Keep only nodes that are connected by valid edges
|
||||
var validNodes = filteredNodes.Where(n => validNodeIds.Contains(n.Id)).ToList();
|
||||
|
||||
// Count removed items
|
||||
int nodesRemoved = filteredNodes.Count - validNodes.Count;
|
||||
int edgesRemoved = filteredEdges.Count - validEdges.Count;
|
||||
|
||||
// Build validation result
|
||||
var validationResult = new MapValidationResultDto
|
||||
{
|
||||
IsValid = nodesRemoved == 0 && edgesRemoved == 0,
|
||||
NodesRemoved = nodesRemoved,
|
||||
EdgesRemoved = edgesRemoved
|
||||
};
|
||||
|
||||
// Add errors for removed nodes
|
||||
foreach (var node in filteredNodes.Where(n => !validNodeIds.Contains(n.Id)))
|
||||
{
|
||||
validationResult.Errors.Add(new ValidationError
|
||||
{
|
||||
Code = "NODE_NO_EDGES",
|
||||
Message = $"Node '{node.NodeId}' has no connected edges in the filtered map",
|
||||
EntityId = node.Id.ToString(),
|
||||
EntityType = "Node"
|
||||
});
|
||||
}
|
||||
|
||||
// Add errors for removed edges
|
||||
foreach (var edge in filteredEdges.Where(e => !validEdges.Contains(e)))
|
||||
{
|
||||
var missingStart = !nodeIds.Contains(edge.StartNodeId);
|
||||
var missingEnd = !nodeIds.Contains(edge.EndNodeId);
|
||||
|
||||
if (missingStart && missingEnd)
|
||||
{
|
||||
validationResult.Errors.Add(new ValidationError
|
||||
{
|
||||
Code = "EDGE_MISSING_BOTH_NODES",
|
||||
Message = $"Edge '{edge.EdgeId}' is missing both start and end nodes",
|
||||
EntityId = edge.Id.ToString(),
|
||||
EntityType = "Edge"
|
||||
});
|
||||
}
|
||||
else if (missingStart)
|
||||
{
|
||||
validationResult.Errors.Add(new ValidationError
|
||||
{
|
||||
Code = "EDGE_MISSING_START_NODE",
|
||||
Message = $"Edge '{edge.EdgeId}' is missing start node",
|
||||
EntityId = edge.Id.ToString(),
|
||||
EntityType = "Edge"
|
||||
});
|
||||
}
|
||||
else if (missingEnd)
|
||||
{
|
||||
validationResult.Errors.Add(new ValidationError
|
||||
{
|
||||
Code = "EDGE_MISSING_END_NODE",
|
||||
Message = $"Edge '{edge.EdgeId}' is missing end node",
|
||||
EntityId = edge.Id.ToString(),
|
||||
EntityType = "Edge"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to DTOs
|
||||
var nodeDtos = validNodes.Select(n => new NodeDto
|
||||
{
|
||||
Id = n.Id,
|
||||
LevelId = n.LevelId,
|
||||
NodeId = n.NodeId,
|
||||
NodeName = n.NodeName,
|
||||
NodeDescription = n.NodeDescription,
|
||||
MapId = n.MapId,
|
||||
X = n.X,
|
||||
Y = n.Y,
|
||||
VehicleProperties = [.. n.VehicleProperties
|
||||
.Where(vp => vp.VehicleTypeId == vehicleTypeId)
|
||||
.Select(vp => new NodeVehiclePropertyDto
|
||||
{
|
||||
Id = vp.Id,
|
||||
NodeId = vp.NodeId,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
Theta = vp.Theta,
|
||||
Actions = vp.Actions
|
||||
})]
|
||||
}).ToList();
|
||||
|
||||
var edgeDtos = validEdges.Select(e => new EdgeDto
|
||||
{
|
||||
Id = e.Id,
|
||||
LevelId = e.LevelId,
|
||||
EdgeId = e.EdgeId,
|
||||
EdgeName = e.EdgeName,
|
||||
EdgeDescription = e.EdgeDescription,
|
||||
StartNodeId = e.StartNodeId,
|
||||
EndNodeId = e.EndNodeId,
|
||||
StartNode = nodeDtos.FirstOrDefault(n => n.Id == e.StartNodeId),
|
||||
EndNode = nodeDtos.FirstOrDefault(n => n.Id == e.EndNodeId),
|
||||
VehicleProperties = [.. e.VehicleProperties
|
||||
.Where(vp => vp.VehicleTypeId == vehicleTypeId)
|
||||
.Select(vp => new EdgeVehiclePropertyDto
|
||||
{
|
||||
Id = vp.Id,
|
||||
EdgeId = vp.EdgeId,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
VehicleOrientation = vp.VehicleOrientation,
|
||||
OrientationType = vp.OrientationType,
|
||||
RotationAllowed = vp.RotationAllowed,
|
||||
RotationAtStartNodeAllowed = vp.RotationAtStartNodeAllowed,
|
||||
RotationAtEndNodeAllowed = vp.RotationAtEndNodeAllowed,
|
||||
MaxSpeed = vp.MaxSpeed,
|
||||
MaxRotationSpeed = vp.MaxRotationSpeed,
|
||||
MinHeight = vp.MinHeight,
|
||||
MaxHeight = vp.MaxHeight,
|
||||
LoadRestriction = null,
|
||||
Actions = vp.Actions
|
||||
})]
|
||||
}).ToList();
|
||||
|
||||
var result = new RobotModelMapDataDto
|
||||
{
|
||||
RobotModelId = robotModel.Id,
|
||||
RobotModelName = robotModel.ModelName,
|
||||
VehicleTypeId = vehicleTypeId,
|
||||
VehicleTypeName = vehicleType.VehicleTypeName,
|
||||
LevelId = levelId,
|
||||
LevelName = levelName,
|
||||
ValidNodes = nodeDtos,
|
||||
ValidEdges = edgeDtos,
|
||||
TotalNodesInLevel = totalNodesInLevel,
|
||||
TotalEdgesInLevel = totalEdgesInLevel,
|
||||
FilteredNodesCount = filteredNodes.Count,
|
||||
FilteredEdgesCount = filteredEdges.Count,
|
||||
ValidationResult = validationResult
|
||||
};
|
||||
|
||||
_logger.Info($"Validated map data for RobotModel '{robotModel.ModelName}': {validNodes.Count} valid nodes, {validEdges.Count} valid edges (removed {nodesRemoved} nodes, {edgesRemoved} edges)");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<MapValidationResultDto> ValidateMapForRobotModelAsync(Guid robotModelId)
|
||||
{
|
||||
var mapData = await GetValidatedMapDataAsync(robotModelId);
|
||||
return mapData.ValidationResult;
|
||||
}
|
||||
|
||||
public async Task<bool> HasValidMapConfigurationAsync(Guid robotModelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var robotModel = await _robotModelService.GetByIdAsync(robotModelId);
|
||||
if (robotModel == null)
|
||||
return false;
|
||||
|
||||
if (!robotModel.VehicleTypeId.HasValue)
|
||||
return false;
|
||||
|
||||
// Check if VehicleType exists and is active
|
||||
var vehicleType = await _vehicleTypeService.GetByIdAsync(robotModel.VehicleTypeId.Value);
|
||||
|
||||
if (vehicleType == null || !vehicleType.IsActive)
|
||||
return false;
|
||||
|
||||
// Validate map data
|
||||
var validationResult = await ValidateMapForRobotModelAsync(robotModelId);
|
||||
return validationResult.IsValid;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error checking valid map configuration for RobotModel {robotModelId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Events;
|
||||
using RobotNet10.FleetManager.Events.Events;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.RobotModel;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing robot models.
|
||||
/// Handles business logic for CRUD operations on robot models.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This service validates business rules such as duplicate model names,
|
||||
/// checks for associated robots before deletion, and provides search functionality.
|
||||
/// </remarks>
|
||||
public class RobotModelService(
|
||||
ApplicationDbContext context,
|
||||
Logger<RobotModelService> logger,
|
||||
IRobotEventBus? eventBus = null) : IRobotModelService
|
||||
{
|
||||
private readonly ApplicationDbContext _context = context;
|
||||
private readonly Logger<RobotModelService> _logger = logger;
|
||||
private readonly IRobotEventBus? _eventBus = eventBus;
|
||||
|
||||
public async Task<RobotModel> CreateAsync(CreateRobotModelRequest request)
|
||||
{
|
||||
// Check if model name already exists
|
||||
if (await ExistsAsync(request.ModelName))
|
||||
{
|
||||
throw new InvalidOperationException($"Robot model with name '{request.ModelName}' already exists.");
|
||||
}
|
||||
|
||||
var robotModel = new RobotModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ModelName = request.ModelName,
|
||||
Length = request.Length,
|
||||
Width = request.Width,
|
||||
ImageWidth = request.ImageWidth,
|
||||
ImageHeight = request.ImageHeight,
|
||||
NavigationPointX = request.NavigationPointX,
|
||||
NavigationPointY = request.NavigationPointY,
|
||||
NavigationType = request.NavigationType,
|
||||
VehicleTypeId = request.VehicleTypeId,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.RobotModels.Add(robotModel);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.Info($"Created robot model {robotModel.ModelName} with ID {robotModel.Id}");
|
||||
return robotModel;
|
||||
}
|
||||
|
||||
public async Task<List<RobotModel>> GetAllAsync()
|
||||
{
|
||||
return await _context.RobotModels
|
||||
.AsNoTracking()
|
||||
.OrderBy(rm => rm.ModelName)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<RobotModel?> GetByIdAsync(Guid id)
|
||||
{
|
||||
return await _context.RobotModels
|
||||
.AsNoTracking()
|
||||
.Include(rm => rm.Robots)
|
||||
.FirstOrDefaultAsync(rm => rm.Id == id);
|
||||
}
|
||||
|
||||
public async Task<RobotModel> UpdateAsync(Guid id, UpdateRobotModelRequest request)
|
||||
{
|
||||
var robotModel = await _context.RobotModels.FindAsync(id) ?? throw new KeyNotFoundException($"Robot model with ID {id} not found.");
|
||||
|
||||
// Check if model name is being changed and if new name already exists
|
||||
if (request.ModelName != null && request.ModelName != robotModel.ModelName)
|
||||
{
|
||||
if (await ExistsAsync(request.ModelName))
|
||||
{
|
||||
throw new InvalidOperationException($"Robot model with name '{request.ModelName}' already exists.");
|
||||
}
|
||||
robotModel.ModelName = request.ModelName;
|
||||
}
|
||||
|
||||
if (request.Length.HasValue)
|
||||
robotModel.Length = request.Length.Value;
|
||||
if (request.Width.HasValue)
|
||||
robotModel.Width = request.Width.Value;
|
||||
if (request.ImageWidth.HasValue)
|
||||
robotModel.ImageWidth = request.ImageWidth.Value;
|
||||
if (request.ImageHeight.HasValue)
|
||||
robotModel.ImageHeight = request.ImageHeight.Value;
|
||||
if (request.NavigationPointX.HasValue)
|
||||
robotModel.NavigationPointX = request.NavigationPointX.Value;
|
||||
if (request.NavigationPointY.HasValue)
|
||||
robotModel.NavigationPointY = request.NavigationPointY.Value;
|
||||
if (request.NavigationType.HasValue)
|
||||
robotModel.NavigationType = request.NavigationType.Value;
|
||||
|
||||
// VehicleTypeId: nullable, can be set or cleared
|
||||
// Note: In ASP.NET Core, if VehicleTypeId is in the JSON request, it will be bound
|
||||
// If not in JSON, it remains the default (null for nullable Guid?)
|
||||
// For simplicity, we always update VehicleTypeId if the request contains it
|
||||
// This allows setting to null by sending "VehicleTypeId": null in JSON
|
||||
// To avoid updating when not provided, we'd need a different approach (e.g., use a wrapper DTO)
|
||||
// For now, we'll always update if the property exists in the request object
|
||||
robotModel.VehicleTypeId = request.VehicleTypeId;
|
||||
|
||||
robotModel.UpdatedDate = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.Info($"Updated robot model {robotModel.ModelName} with ID {robotModel.Id}");
|
||||
|
||||
// Publish event for cache invalidation
|
||||
if (_eventBus != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get all robots using this model
|
||||
var affectedRobots = await _context.Robots
|
||||
.Where(r => r.ModelId == robotModel.Id)
|
||||
.Select(r => r.RobotId)
|
||||
.ToListAsync();
|
||||
|
||||
var eventData = new RobotModelUpdatedEvent
|
||||
{
|
||||
ModelId = robotModel.Id,
|
||||
AffectedRobotIds = affectedRobots
|
||||
};
|
||||
|
||||
_eventBus.PublishRobotModelUpdated(eventData);
|
||||
_logger.Debug($"Published RobotModelUpdated event for ModelId {robotModel.Id} affecting {affectedRobots.Count} robot(s)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error publishing RobotModelUpdated event: {ex.Message}");
|
||||
// Don't fail the update if event publishing fails
|
||||
}
|
||||
}
|
||||
|
||||
return robotModel;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var robotModel = await _context.RobotModels
|
||||
.Include(rm => rm.Robots)
|
||||
.FirstOrDefaultAsync(rm => rm.Id == id);
|
||||
|
||||
if (robotModel == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there are any robots using this model
|
||||
if (robotModel.Robots.Count != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot delete robot model '{robotModel.ModelName}' because it is being used by {robotModel.Robots.Count} robot(s).");
|
||||
}
|
||||
|
||||
_context.RobotModels.Remove(robotModel);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.Info($"Deleted robot model {robotModel.ModelName} with ID {robotModel.Id}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string modelName)
|
||||
{
|
||||
return await _context.RobotModels
|
||||
.AnyAsync(rm => rm.ModelName == modelName);
|
||||
}
|
||||
|
||||
public async Task<List<RobotModel>> SearchAsync(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return await GetAllAsync();
|
||||
}
|
||||
|
||||
var lowerQuery = query.ToLowerInvariant();
|
||||
return await _context.RobotModels
|
||||
.AsNoTracking()
|
||||
.Where(rm => rm.ModelName.ToLower().Contains(lowerQuery))
|
||||
.OrderBy(rm => rm.ModelName)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<RobotModelUsageInfoDto> GetUsageInfoAsync(Guid id)
|
||||
{
|
||||
var robotModel = await _context.RobotModels
|
||||
.AsNoTracking()
|
||||
.Include(rm => rm.Robots)
|
||||
.FirstOrDefaultAsync(rm => rm.Id == id);
|
||||
|
||||
return robotModel == null
|
||||
? throw new KeyNotFoundException($"Robot model with ID {id} not found.")
|
||||
: new RobotModelUsageInfoDto
|
||||
{
|
||||
Id = robotModel.Id,
|
||||
ModelName = robotModel.ModelName,
|
||||
RobotCount = robotModel.Robots.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Events;
|
||||
using RobotNet10.FleetManager.Events.Events;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Robot;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing robots.
|
||||
/// Handles business logic for CRUD operations on robots.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This service validates business rules such as duplicate robot IDs,
|
||||
/// ensures referenced robot models exist, and provides filtering and search functionality.
|
||||
/// </remarks>
|
||||
public class RobotService(
|
||||
ApplicationDbContext context,
|
||||
Logger<RobotService> logger,
|
||||
IRobotEventBus? eventBus = null) : IRobotService
|
||||
{
|
||||
private readonly ApplicationDbContext _context = context;
|
||||
private readonly Logger<RobotService> _logger = logger;
|
||||
private readonly IRobotEventBus? _eventBus = eventBus;
|
||||
|
||||
public async Task<Robot> CreateAsync(CreateRobotRequest request)
|
||||
{
|
||||
// Check if robot ID already exists
|
||||
if (await ExistsAsync(request.RobotId))
|
||||
{
|
||||
throw new InvalidOperationException($"Robot with ID '{request.RobotId}' already exists.");
|
||||
}
|
||||
|
||||
// Verify that the model exists
|
||||
var modelExists = await _context.RobotModels.AnyAsync(rm => rm.Id == request.ModelId);
|
||||
if (!modelExists)
|
||||
{
|
||||
throw new KeyNotFoundException($"Robot model with ID {request.ModelId} not found.");
|
||||
}
|
||||
|
||||
var robot = new Robot
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RobotId = request.RobotId,
|
||||
Name = request.Name,
|
||||
ModelId = request.ModelId,
|
||||
MapId = request.MapId,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Robots.Add(robot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.Info($"Created robot {robot.Name} with RobotId {robot.RobotId} and ID {robot.Id}");
|
||||
return robot;
|
||||
}
|
||||
|
||||
public async Task<List<Robot>> GetAllAsync(Guid? modelId = null, Guid? mapId = null)
|
||||
{
|
||||
var query = _context.Robots.AsNoTracking().Include(r => r.Model).AsQueryable();
|
||||
|
||||
if (modelId.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.ModelId == modelId.Value);
|
||||
}
|
||||
|
||||
if (mapId.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.MapId == mapId.Value);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(r => r.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Robot?> GetByIdAsync(Guid id)
|
||||
{
|
||||
return await _context.Robots
|
||||
.AsNoTracking()
|
||||
.Include(r => r.Model)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
}
|
||||
|
||||
public async Task<Robot?> GetByRobotIdAsync(string robotId)
|
||||
{
|
||||
return await _context.Robots
|
||||
.AsNoTracking()
|
||||
.Include(r => r.Model)
|
||||
.FirstOrDefaultAsync(r => r.RobotId == robotId);
|
||||
}
|
||||
|
||||
public async Task<Robot> UpdateAsync(Guid id, UpdateRobotRequest request)
|
||||
{
|
||||
var robot = await _context.Robots.FindAsync(id) ?? throw new KeyNotFoundException($"Robot with ID {id} not found.");
|
||||
|
||||
// Check if robot ID is being changed and if new ID already exists
|
||||
if (request.RobotId != null && request.RobotId != robot.RobotId)
|
||||
{
|
||||
if (await ExistsAsync(request.RobotId))
|
||||
{
|
||||
throw new InvalidOperationException($"Robot with ID '{request.RobotId}' already exists.");
|
||||
}
|
||||
robot.RobotId = request.RobotId;
|
||||
}
|
||||
|
||||
if (request.Name != null)
|
||||
robot.Name = request.Name;
|
||||
|
||||
if (request.ModelId.HasValue)
|
||||
{
|
||||
// Verify that the model exists
|
||||
var modelExists = await _context.RobotModels.AnyAsync(rm => rm.Id == request.ModelId.Value);
|
||||
if (!modelExists)
|
||||
{
|
||||
throw new KeyNotFoundException($"Robot model with ID {request.ModelId.Value} not found.");
|
||||
}
|
||||
|
||||
// Check if ModelId is actually changing
|
||||
var previousModelId = robot.ModelId;
|
||||
if (previousModelId != request.ModelId.Value)
|
||||
{
|
||||
robot.ModelId = request.ModelId.Value;
|
||||
|
||||
// Publish event for cache invalidation
|
||||
if (_eventBus != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var eventData = new RobotModelIdChangedEvent
|
||||
{
|
||||
RobotId = robot.RobotId,
|
||||
PreviousModelId = previousModelId,
|
||||
NewModelId = request.ModelId.Value
|
||||
};
|
||||
|
||||
_eventBus.PublishRobotModelIdChanged(eventData);
|
||||
_logger.Debug($"Published RobotModelIdChanged event for robot {robot.RobotId} (from {previousModelId} to {request.ModelId.Value})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error publishing RobotModelIdChanged event: {ex.Message}");
|
||||
// Don't fail the update if event publishing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MapId: if provided (including null), update it
|
||||
// Note: In C#, nullable Guid? means: HasValue = true means a value was provided (could be Guid.Empty or a valid Guid)
|
||||
// To distinguish between "not provided" and "explicitly set to null", we'd need a different approach
|
||||
// For now, we'll only update MapId if it has a value (non-null)
|
||||
if (request.MapId.HasValue)
|
||||
{
|
||||
robot.MapId = request.MapId.Value;
|
||||
}
|
||||
|
||||
robot.UpdatedDate = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.Info($"Updated robot {robot.Name} with RobotId {robot.RobotId} and ID {robot.Id}");
|
||||
return robot;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var robot = await _context.Robots.FindAsync(id);
|
||||
if (robot == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_context.Robots.Remove(robot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.Info($"Deleted robot {robot.Name} with RobotId {robot.RobotId} and ID {robot.Id}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<List<Robot>> GetByModelIdAsync(Guid modelId)
|
||||
{
|
||||
return await _context.Robots
|
||||
.AsNoTracking()
|
||||
.Include(r => r.Model)
|
||||
.Where(r => r.ModelId == modelId)
|
||||
.OrderBy(r => r.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<Robot>> SearchAsync(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return await GetAllAsync();
|
||||
}
|
||||
|
||||
var lowerQuery = query.ToLowerInvariant();
|
||||
return await _context.Robots
|
||||
.AsNoTracking()
|
||||
.Include(r => r.Model)
|
||||
.Where(r => r.RobotId.ToLower().Contains(lowerQuery) || r.Name.ToLower().Contains(lowerQuery))
|
||||
.OrderBy(r => r.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string robotId)
|
||||
{
|
||||
return await _context.Robots
|
||||
.AnyAsync(r => r.RobotId == robotId);
|
||||
}
|
||||
|
||||
public async Task<List<Robot>> GetByModelNameAsync(string modelName)
|
||||
{
|
||||
return await _context.Robots
|
||||
.AsNoTracking()
|
||||
.Include(r => r.Model)
|
||||
.Where(r => r.Name == modelName)
|
||||
.OrderBy(r => r.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using RobotNet10.FleetManager.Script;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.Script;
|
||||
|
||||
public class ScriptLayoutManager : ILayoutManager
|
||||
{
|
||||
public Task<RobotNet.VDA5050.InstantAction.Action> GetAction(string layout, string version, string level, string name, string robotId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<INode> GetNode(string layout, string version, string level, string name)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IStation> GetStation(string layout, string version, string level, string name)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Services.RobotController;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.Script;
|
||||
|
||||
public class ScriptRobot(string robotId, string robotName, Guid modelId, Guid? mapId, IRobotController robotController) : IRobot
|
||||
{
|
||||
public string RobotId { get; } = robotId;
|
||||
|
||||
public string Name { get; } = robotName;
|
||||
|
||||
public Guid ModelId { get; } = modelId;
|
||||
|
||||
public Guid? MapId { get; } = mapId;
|
||||
|
||||
public RobotState State => new( robotController.IsReady,
|
||||
robotController.Data.State?.BatteryState.BatteryVoltage ?? 0,
|
||||
robotController.Data.State?.Loads ?? [],
|
||||
robotController.Data.State?.BatteryState.Charging ?? false,
|
||||
robotController.Data.Visualization?.AgvPosition.X ?? 0,
|
||||
robotController.Data.Visualization?.AgvPosition.Y ?? 0,
|
||||
robotController.Data.Visualization?.AgvPosition.Theta ?? 0);
|
||||
|
||||
public Task AbortMovement()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<RobotResult> Execute(RobotNet.VDA5050.InstantAction.Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await robotController.SendInstantActionAsync(action, cancellationToken);
|
||||
return new(result.IsSuccess, result.Message);
|
||||
}
|
||||
|
||||
public async Task<RobotResult> MoveToNode(string nodeName, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await robotController.MoveToNodeAsync(nodeName, null, cancellationToken);
|
||||
return new(result.IsSuccess, result.Message);
|
||||
}
|
||||
|
||||
public async Task<RobotResult> MoveToNode(string nodeName, double lastAngle, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await robotController.MoveToNodeAsync(nodeName, lastAngle, cancellationToken);
|
||||
return new(result.IsSuccess, result.Message);
|
||||
}
|
||||
|
||||
public async Task<RobotResult> MoveToStation(string stationName, StationAction action, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await robotController.MoveToStationAsync(stationName, action, cancellationToken);
|
||||
return new(result.IsSuccess, result.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Services.RobotManager;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.Script;
|
||||
|
||||
public class ScriptRobotmanager(IRobotManagerService RobotManager, IServiceScopeFactory ScopeFactory, IOrderControlService OrderControlService) : IRobotManager
|
||||
{
|
||||
public async Task<IRobot?> GetRobotById(string robotId)
|
||||
{
|
||||
using var scope = ScopeFactory.CreateScope();
|
||||
var _robotService = scope.ServiceProvider.GetRequiredService<IRobotService>();
|
||||
var robotDb = await _robotService.GetByRobotIdAsync(robotId);
|
||||
if (robotDb is null) return null;
|
||||
var robotController = RobotManager.GetRobotController(robotId);
|
||||
if (robotController is null) return null;
|
||||
return new ScriptRobot(robotDb.RobotId, robotDb.Name, robotDb.ModelId, robotDb.MapId, robotController);
|
||||
}
|
||||
|
||||
public Task<RobotOrderStatus> GetRobotOrderStatus(string robotId)
|
||||
{
|
||||
var orderStatus = OrderControlService.GetRobotOrderStatus(robotId);
|
||||
return orderStatus switch
|
||||
{
|
||||
OrderStatus.IsError => Task.FromResult(RobotOrderStatus.IsError),
|
||||
OrderStatus.IsCompleted => Task.FromResult(RobotOrderStatus.IsCompleted),
|
||||
OrderStatus.IsProccessing => Task.FromResult(RobotOrderStatus.IsProccessing),
|
||||
OrderStatus.IsCanceled => Task.FromResult(RobotOrderStatus.IsCanceled),
|
||||
_ => Task.FromResult(RobotOrderStatus.Empty),
|
||||
};
|
||||
}
|
||||
|
||||
public Task<RobotState?> GetRobotState(string robotId)
|
||||
{
|
||||
var robotController = RobotManager.GetRobotController(robotId);
|
||||
if (robotController is null || robotController.Data is null || robotController.Data.State is null || robotController.Data.Visualization is null) return Task.FromResult<RobotState?>(null);
|
||||
return Task.FromResult<RobotState?>(new RobotState(robotController.IsReady,
|
||||
robotController.Data.State.BatteryState.BatteryVoltage ?? 0,
|
||||
robotController.Data.State.Loads,
|
||||
robotController.Data.State.BatteryState.Charging,
|
||||
robotController.Data.Visualization.AgvPosition.X,
|
||||
robotController.Data.Visualization.AgvPosition.Y,
|
||||
robotController.Data.Visualization.AgvPosition.Theta));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> SearchRobots(string layout, string version, string level, string model)
|
||||
=> await RobotManager.GetAvailableRobots(layout, version, level, model, state => true);
|
||||
|
||||
public async Task<IEnumerable<string>> SearchRobots(string layout, string version, string level, string model, Expression<Func<RobotState, bool>> expr)
|
||||
=> await RobotManager.GetAvailableRobots(layout, version, level, model, expr.Compile());
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using RobotNet10.FleetManager.Models;
|
||||
using RobotNet10.FleetManager.Script;
|
||||
using RobotNet10.FleetManager.Script.Shared;
|
||||
using RobotNet10.ScriptEngine.Helpers;
|
||||
using RobotNet10.ScriptEngine.Shared;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services;
|
||||
|
||||
public class ScriptEngineResource(IRobotManager robotManager, ILayoutManager layoutManager) : IScriptEngineResource
|
||||
{
|
||||
public Type AppGlobalType => FleetManagerScriptEngineResource.GlobalType;
|
||||
|
||||
public ImmutableArray<string> UsingNamespaces => FleetManagerScriptEngineResource.UsingNamespaces;
|
||||
|
||||
public ImmutableArray<string> Modules => FleetManagerScriptEngineResource.Modules;
|
||||
|
||||
public ImmutableArray<string> DocModules => FleetManagerScriptEngineResource.DocModules;
|
||||
|
||||
public IDictionary<string, object?> GetMissionGlobals(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var globals = new FleetManagerScriptGlobals(robotManager, layoutManager);
|
||||
return ScriptHelper.ConvertGlobalsToDictionary(globals, typeof(IFleetManagerScriptGlobals));
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> GetTaskGlobals()
|
||||
{
|
||||
var globals = new FleetManagerScriptGlobals(robotManager, layoutManager);
|
||||
return ScriptHelper.ConvertGlobalsToDictionary(globals, typeof(IFleetManagerScriptGlobals));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,923 @@
|
||||
using RobotNet.VDA5050.Order;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet10.Common;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Services.ConfigManager;
|
||||
using RobotNet10.FleetManager.Services.OpenACS;
|
||||
using RobotNet10.FleetManager.Services.RobotController;
|
||||
using RobotNet10.FleetManager.Services.RobotManager;
|
||||
using RobotNet10.FleetManager.Services.TrafficControl.Models;
|
||||
using RobotNet10.MapManager.Services;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RobotNet10.FleetManager.Services.TrafficControl.ACS;
|
||||
|
||||
public class OrderACSControl : IOrderControlService, IDisposable
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IACSTrafficConfig _acsTrafficConfig;
|
||||
private readonly TrafficACS _trafficACS;
|
||||
private readonly Logger<OrderACSControl> _logger;
|
||||
private readonly ILogger<OrderACSControl> _loggerTimer;
|
||||
|
||||
// Store order state per robot
|
||||
private readonly ConcurrentDictionary<string, OrderACSState> _orderStates = new();
|
||||
|
||||
private WatchTimerAsync<OrderACSControl>? _processingTimer;
|
||||
private readonly Lock _timerLock = new();
|
||||
private bool _disposed = false;
|
||||
|
||||
public OrderACSControl(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IACSTrafficConfig acsTrafficConfig,
|
||||
TrafficACS trafficACS,
|
||||
Logger<OrderACSControl> logger,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
|
||||
_acsTrafficConfig = acsTrafficConfig ?? throw new ArgumentNullException(nameof(acsTrafficConfig));
|
||||
_trafficACS = trafficACS ?? throw new ArgumentNullException(nameof(trafficACS));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_loggerTimer = loggerFactory?.CreateLogger<OrderACSControl>() ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
|
||||
// Subscribe to config changes
|
||||
_acsTrafficConfig.ConfigChanged += OnConfigChanged;
|
||||
|
||||
_logger.Info($"Started OrderACSControl processing timer with interval {_acsTrafficConfig.TrafficInterval}ms");
|
||||
}
|
||||
|
||||
private void OnConfigChanged(object? sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newInterval = _acsTrafficConfig.TrafficInterval;
|
||||
if (newInterval == _processingTimer?.Interval) return;
|
||||
_logger.Info($"ACSTrafficConfig changed, updating timer interval to {newInterval}ms");
|
||||
|
||||
lock (_timerLock)
|
||||
{
|
||||
// Stop old timer
|
||||
_processingTimer?.Stop();
|
||||
_processingTimer?.Dispose();
|
||||
|
||||
// Start new timer with new interval
|
||||
StartProcessingTimer();
|
||||
}
|
||||
|
||||
_logger.Info($"OrderACSControl processing timer updated to interval {newInterval}ms");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error updating timer interval: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartProcessingTimer()
|
||||
{
|
||||
var interval = _acsTrafficConfig.TrafficInterval;
|
||||
_processingTimer = new WatchTimerAsync<OrderACSControl>(
|
||||
interval,
|
||||
ProcessAllOrdersAsync,
|
||||
_loggerTimer
|
||||
);
|
||||
_processingTimer.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
// Unsubscribe from config changes
|
||||
_acsTrafficConfig.ConfigChanged -= OnConfigChanged;
|
||||
|
||||
lock (_timerLock)
|
||||
{
|
||||
_processingTimer?.Stop();
|
||||
_processingTimer?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_logger.Info("Stopped OrderACSControl processing timer");
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
}
|
||||
|
||||
private async Task ProcessAllOrdersAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Process all active orders
|
||||
foreach (var kvp in _orderStates)
|
||||
{
|
||||
var robotId = kvp.Key;
|
||||
var orderState = kvp.Value;
|
||||
|
||||
if (orderState.Status != OrderStatus.IsProccessing)
|
||||
{
|
||||
// Skip non-processing orders
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get robot controller and data using service scope to avoid circular dependency
|
||||
IRobotController? robotController = null;
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var robotManagerService = scope.ServiceProvider.GetRequiredService<IRobotManagerService>();
|
||||
robotController = robotManagerService.GetRobotController(robotId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"ProcessAllOrdersAsync: Error getting robot controller for {robotId}: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (robotController == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if robot is online
|
||||
if (!robotController.IsOnline)
|
||||
{
|
||||
// Check if robot has been offline for more than 1 minute
|
||||
var offlineDuration = DateTime.UtcNow - orderState.LastUpdated;
|
||||
if (offlineDuration.TotalMinutes > 1)
|
||||
{
|
||||
orderState.Status = OrderStatus.IsError;
|
||||
orderState.Error = $"Robot {robotId} has been offline for more than 1 minute";
|
||||
orderState.LastUpdated = DateTime.UtcNow;
|
||||
_logger.Warning($"CheckRobotOnline: Order for robot {robotId} marked as Error due to offline timeout ({offlineDuration.TotalMinutes:F1} minutes)");
|
||||
continue;
|
||||
}
|
||||
// Robot is offline but less than 1 minute, skip processing
|
||||
continue;
|
||||
}
|
||||
|
||||
var robotData = robotController.Data;
|
||||
var stateMsg = robotData?.State;
|
||||
|
||||
if (stateMsg == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update last updated time
|
||||
orderState.LastUpdated = DateTime.UtcNow;
|
||||
|
||||
// Check if robot has reached a mapped node
|
||||
var currentLastNodeId = stateMsg.LastNodeId;
|
||||
await CheckAndProcessMappedNodesAsync(robotId, currentLastNodeId, orderState, stateMsg);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"ProcessAllOrdersAsync: Error processing orders: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public OrderStatus GetRobotOrderStatus(string robotId)
|
||||
{
|
||||
if (_orderStates.TryGetValue(robotId, out var state))
|
||||
{
|
||||
return state.Status;
|
||||
}
|
||||
return OrderStatus.Empty; // No order found
|
||||
}
|
||||
|
||||
public async Task<bool> CreateRobotOrderAsync(string robotId, RobotRoute route)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(robotId))
|
||||
{
|
||||
_logger.Warning("CreateRobotOrderAsync: robotId is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (route == null || route.FullRoute == null || route.FullRoute.Count == 0)
|
||||
{
|
||||
_logger.Warning($"CreateRobotOrderAsync: Invalid route for robot {robotId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find all nodes that are mapped to ACS zones (IN and OUT separately)
|
||||
var (inMappedNodes, outMappedNodes) = await FindMappedNodesAsync(route);
|
||||
|
||||
// Find first IN mapped node index (only IN nodes affect Base/Horizon calculation)
|
||||
var firstMappedNodeIndex = FindFirstMappedNodeIndex(route, inMappedNodes);
|
||||
|
||||
int baseSegmentCount;
|
||||
if (inMappedNodes.Count == 0 || firstMappedNodeIndex < 0)
|
||||
{
|
||||
// No IN mapped nodes, base = full route
|
||||
baseSegmentCount = route.FullRoute.Count;
|
||||
_logger.Info($"CreateRobotOrderAsync: No IN mapped nodes found for robot {robotId}, base = full route");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate Base: segments from start to first mapped node + 1 segment
|
||||
baseSegmentCount = firstMappedNodeIndex + 3;
|
||||
if (baseSegmentCount > route.FullRoute.Count)
|
||||
{
|
||||
baseSegmentCount = route.FullRoute.Count;
|
||||
}
|
||||
}
|
||||
|
||||
// Split route into Base and Horizon
|
||||
route.Base = [.. route.FullRoute.Take(baseSegmentCount)];
|
||||
route.Horizon = [.. route.FullRoute.Skip(baseSegmentCount)];
|
||||
|
||||
// Mark base segments as released
|
||||
foreach (var segment in route.Base)
|
||||
{
|
||||
segment.Released = true;
|
||||
}
|
||||
|
||||
_logger.Info($"CreateRobotOrderAsync: Order created for robot {robotId}, Base segments: {route.Base.Count}, Horizon segments: {route.Horizon.Count}, IN mapped nodes: {inMappedNodes.Count}, OUT mapped nodes: {outMappedNodes.Count}");
|
||||
|
||||
// Send order to robot
|
||||
var send = await SendOrderToRobotAsync(robotId, route, isInitial: true);
|
||||
if (send)
|
||||
{
|
||||
// Create order state
|
||||
var orderState = new OrderACSState
|
||||
{
|
||||
RobotId = robotId,
|
||||
Status = OrderStatus.IsProccessing,
|
||||
Route = route,
|
||||
InMappedNodes = inMappedNodes,
|
||||
OutMappedNodes = outMappedNodes,
|
||||
CurrentInMappedNodeIndex = 0,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastUpdated = DateTime.UtcNow,
|
||||
ZoneRequestInCompleting = [],
|
||||
ZoneRequestOutCompleting = [],
|
||||
};
|
||||
|
||||
_orderStates.AddOrUpdate(robotId, orderState, (key, old) => orderState);
|
||||
|
||||
}
|
||||
|
||||
return send;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"CreateRobotOrderAsync: Error creating order for robot {robotId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(List<(string NodeIdString, string ZoneId)> InMappedNodes, List<(string NodeIdString, string ZoneId)> OutMappedNodes)> FindMappedNodesAsync(RobotRoute route)
|
||||
{
|
||||
var inMappedNodes = new List<(string NodeIdString, string ZoneId)>();
|
||||
var outMappedNodes = new List<(string NodeIdString, string ZoneId)>();
|
||||
var acsZoneMapping = _acsTrafficConfig.ACSZoneMaping;
|
||||
var acsOutMapping = _acsTrafficConfig.ACSOutMaping;
|
||||
|
||||
// Get all unique NodeIds (Guid) from route segments
|
||||
var nodeIds = route.FullRoute
|
||||
.Where(s => s.VdaNode != null)
|
||||
.Select(s => s.NodeId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (nodeIds.Count == 0)
|
||||
{
|
||||
return (inMappedNodes, outMappedNodes);
|
||||
}
|
||||
|
||||
// Create mapping: NodeId (Guid) -> NodeName
|
||||
var nodeIdToNodeName = new Dictionary<Guid, string?>();
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var nodeService = scope.ServiceProvider.GetRequiredService<INodeService>();
|
||||
|
||||
foreach (var nodeId in nodeIds)
|
||||
{
|
||||
var node = await nodeService.GetByIdAsync(nodeId, includeVehicleProperties: false);
|
||||
if (node != null && !string.IsNullOrEmpty(node.NodeName))
|
||||
{
|
||||
nodeIdToNodeName[nodeId] = node.NodeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"FindMappedNodesAsync: Error getting NodeName from database: {ex.Message}");
|
||||
// Continue with empty mapping - will skip nodes without NodeName
|
||||
}
|
||||
|
||||
foreach (var segment in route.FullRoute)
|
||||
{
|
||||
if (segment.VdaNode != null)
|
||||
{
|
||||
var nodeIdString = segment.VdaNode.NodeId;
|
||||
|
||||
// Get NodeName from mapping (using NodeId Guid)
|
||||
if (!nodeIdToNodeName.TryGetValue(segment.NodeId, out var nodeName) || string.IsNullOrEmpty(nodeName))
|
||||
{
|
||||
// Skip if NodeName not found
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if node is in ACSZoneMapping (RequestIn) using NodeName
|
||||
if (acsZoneMapping.TryGetValue(nodeName, out var zoneId))
|
||||
{
|
||||
inMappedNodes.Add((nodeIdString, zoneId));
|
||||
}
|
||||
|
||||
// Check if node is in ACSOutMapping (RequestOut) using NodeName
|
||||
if (acsOutMapping.TryGetValue(nodeName, out var outZoneId))
|
||||
{
|
||||
outMappedNodes.Add((nodeIdString, outZoneId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (inMappedNodes, outMappedNodes);
|
||||
}
|
||||
|
||||
private static int FindFirstMappedNodeIndex(RobotRoute route, List<(string NodeIdString, string ZoneId)> mappedNodes)
|
||||
{
|
||||
if (mappedNodes.Count == 0) return -1;
|
||||
|
||||
(string NodeIdString, _) = mappedNodes[0];
|
||||
|
||||
for (int i = 0; i < route.FullRoute.Count; i++)
|
||||
{
|
||||
var segment = route.FullRoute[i];
|
||||
if (segment.VdaNode != null && segment.VdaNode.NodeId == NodeIdString)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private async Task CheckAndProcessMappedNodesAsync(string robotId, string lastNodeId, OrderACSState orderState, StateMsg stateMsg)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var (NodeIdString, ZoneId) in orderState.OutMappedNodes)
|
||||
{
|
||||
if (lastNodeId == NodeIdString && !string.IsNullOrEmpty(ZoneId) && !orderState.ZoneRequestOutCompleting.Contains(ZoneId) && !orderState.ZoneRequestOutCompleted.Contains(ZoneId))
|
||||
{
|
||||
orderState.ZoneRequestOutCompleting.Add(ZoneId);
|
||||
}
|
||||
}
|
||||
var requestOut = ProcessRequestOutAsync(robotId, orderState);
|
||||
|
||||
foreach (var (NodeIdString, ZoneId) in orderState.InMappedNodes)
|
||||
{
|
||||
if (lastNodeId == NodeIdString && !string.IsNullOrEmpty(ZoneId) && !orderState.ZoneRequestInCompleting.Contains(ZoneId) && !orderState.ZoneRequestInCompleted.Contains(ZoneId))
|
||||
{
|
||||
orderState.ZoneRequestInCompleting.Add(ZoneId);
|
||||
}
|
||||
}
|
||||
await ProcessRequestInAsync(robotId, orderState);
|
||||
|
||||
await requestOut.WaitAsync(CancellationToken.None);
|
||||
|
||||
// Check if order is completed
|
||||
CheckOrderCompletion(robotId, orderState, stateMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"CheckAndProcessMappedNodes: Error for robot {robotId}: {ex.Message}");
|
||||
orderState.Status = OrderStatus.IsError;
|
||||
orderState.Error = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessRequestInAsync(string robotId, OrderACSState orderState)
|
||||
{
|
||||
string[] zoneIdsIn = [.. orderState.ZoneRequestInCompleting];
|
||||
if (zoneIdsIn.Length == 0) return;
|
||||
foreach (var zoneId in zoneIdsIn)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
if (orderState.ZoneRequestInCompleted.Contains(zoneId)) continue;
|
||||
_logger.Info($"ProcessRequestInAsync: Robot {robotId} requesting into zone {zoneId}");
|
||||
|
||||
var result = await _trafficACS.RequestIn(robotId, zoneId);
|
||||
|
||||
if (result.IsSuccess && result.Data)
|
||||
{
|
||||
// RequestIn successful - save zone to cache for future RequestOut validation
|
||||
orderState.ZoneRequestInCompleted.Add(zoneId);
|
||||
orderState.ZoneRequestInCompleting.Remove(zoneId);
|
||||
orderState.ZoneRequestOutCompleted.Remove(zoneId);
|
||||
orderState.LastUpdated = DateTime.UtcNow;
|
||||
|
||||
_logger.Info($"ProcessRequestInAsync: Robot {robotId} successfully requested into zone {zoneId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// RequestIn failed, just log warning - will retry on next timer cycle
|
||||
_logger.Warning($"ProcessRequestInAsync: Robot {robotId} failed to request into zone {zoneId}: {result.Message}. Will retry on next cycle.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"ProcessRequestInAsync: Error for robot {robotId}, zone {zoneId}: {ex.Message}. Will retry on next cycle.");
|
||||
}
|
||||
}
|
||||
if (orderState.ZoneRequestInCompleting.Count == 0)
|
||||
{
|
||||
orderState.CurrentInMappedNodeIndex++;
|
||||
await TryReleaseNextHorizonSegmentAsync(robotId, orderState);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessRequestOutAsync(string robotId, OrderACSState orderState)
|
||||
{
|
||||
string[] zoneIdsOut = [.. orderState.ZoneRequestOutCompleting];
|
||||
if (zoneIdsOut.Length == 0) return;
|
||||
foreach (var zoneId in zoneIdsOut)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (orderState.ZoneRequestOutCompleted.Contains(zoneId)) continue;
|
||||
_logger.Info($"ProcessRequestOutAsync: Robot {robotId} requesting out of zone {zoneId}");
|
||||
|
||||
var result = await _trafficACS.RequestOut(robotId, zoneId);
|
||||
|
||||
if (result.IsSuccess && result.Data)
|
||||
{
|
||||
orderState.LastUpdated = DateTime.UtcNow;
|
||||
orderState.ZoneRequestOutCompleted.Add(zoneId);
|
||||
orderState.ZoneRequestOutCompleting.Remove(zoneId);
|
||||
orderState.ZoneRequestInCompleted.Remove(zoneId);
|
||||
|
||||
_logger.Info($"ProcessRequestOutAsync: Robot {robotId} successfully requested out of zone {zoneId}");
|
||||
// Note: OUT does NOT affect Base/Horizon, so we don't call TryReleaseNextHorizonSegmentAsync
|
||||
}
|
||||
else
|
||||
{
|
||||
// RequestOut failed - will retry on next timer cycle until successful
|
||||
// OUT must retry until successful (unlike IN which can be skipped)
|
||||
_logger.Warning($"ProcessRequestOutAsync: Robot {robotId} failed to request out of zone {zoneId}: {result.Message}. Will retry on next cycle until successful.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// RequestOut error - will retry on next timer cycle until successful
|
||||
_logger.Error($"ProcessRequestOutAsync: Error for robot {robotId}, zone {zoneId}: {ex.Message}. Will retry on next cycle until successful.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryReleaseNextHorizonSegmentAsync(string robotId, OrderACSState orderState)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool horizonUpdated = false;
|
||||
int baseCountBeforeRelease = orderState.Route.Base.Count; // Store base count before release
|
||||
|
||||
// Check if there are more IN mapped nodes to process (only IN affects Base/Horizon)
|
||||
if (orderState.CurrentInMappedNodeIndex >= orderState.InMappedNodes.Count)
|
||||
{
|
||||
// All mapped nodes processed, check if we can release all remaining horizon
|
||||
if (orderState.Route.Horizon.Count > 0)
|
||||
{
|
||||
// Move all remaining horizon to base
|
||||
var segmentsToMove = orderState.Route.Horizon.ToList();
|
||||
orderState.Route.Base.AddRange(segmentsToMove);
|
||||
orderState.Route.Horizon.Clear();
|
||||
|
||||
// Only set Released = true for newly released segments
|
||||
foreach (var segment in segmentsToMove)
|
||||
{
|
||||
segment.Released = true;
|
||||
}
|
||||
|
||||
orderState.LastUpdated = DateTime.UtcNow;
|
||||
horizonUpdated = true;
|
||||
_logger.Info($"TryReleaseNextHorizonSegmentAsync: Released all remaining horizon segments for robot {robotId}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find next IN mapped node
|
||||
var (NodeIdString, _) = orderState.InMappedNodes[orderState.CurrentInMappedNodeIndex];
|
||||
|
||||
// Find index of next mapped node in full route
|
||||
int nextMappedNodeIndex = -1;
|
||||
for (int i = 0; i < orderState.Route.FullRoute.Count; i++)
|
||||
{
|
||||
var segment = orderState.Route.FullRoute[i];
|
||||
if (segment.VdaNode != null && segment.VdaNode.NodeId == NodeIdString)
|
||||
{
|
||||
nextMappedNodeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMappedNodeIndex >= 0)
|
||||
{
|
||||
// Calculate how many segments to release: from current base end to next mapped node + 1
|
||||
var currentBaseEndIndex = orderState.Route.Base.Count;
|
||||
var segmentsToRelease = nextMappedNodeIndex - currentBaseEndIndex + 3;
|
||||
|
||||
if (segmentsToRelease > 0 && segmentsToRelease <= orderState.Route.Horizon.Count)
|
||||
{
|
||||
// Release segments to base
|
||||
var segmentsToMove = orderState.Route.Horizon.Take(segmentsToRelease).ToList();
|
||||
orderState.Route.Base.AddRange(segmentsToMove);
|
||||
orderState.Route.Horizon.RemoveRange(0, segmentsToRelease);
|
||||
|
||||
foreach (var segment in segmentsToMove)
|
||||
{
|
||||
segment.Released = true;
|
||||
}
|
||||
|
||||
orderState.LastUpdated = DateTime.UtcNow;
|
||||
horizonUpdated = true;
|
||||
_logger.Info($"TryReleaseNextHorizonSegmentAsync: Released {segmentsToRelease} segments to base for robot {robotId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If horizon was updated, send OrderUpdate to robot
|
||||
if (horizonUpdated)
|
||||
{
|
||||
await SendOrderToRobotAsync(robotId, orderState.Route, isInitial: false, baseCountBeforeRelease);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"TryReleaseNextHorizonSegmentAsync: Error for robot {robotId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> SendOrderToRobotAsync(string robotId, RobotRoute route, bool isInitial, int baseCountBeforeRelease = -1)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get robot controller using service scope to avoid circular dependency
|
||||
IRobotController? robotController = null;
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var robotManagerService = scope.ServiceProvider.GetRequiredService<IRobotManagerService>();
|
||||
robotController = robotManagerService.GetRobotController(robotId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"SendOrderToRobotAsync: Error getting robot controller for {robotId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (robotController == null)
|
||||
{
|
||||
_logger.Warning($"SendOrderToRobotAsync: Robot controller not found for robot {robotId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build nodes and edges
|
||||
var nodes = new List<Node>();
|
||||
var edges = new List<Edge>();
|
||||
|
||||
if (isInitial)
|
||||
{
|
||||
// Initial order: send ALL Base + Horizon segments
|
||||
// Add Base segments (released = true)
|
||||
foreach (var segment in route.Base)
|
||||
{
|
||||
if (segment.VdaNode != null)
|
||||
{
|
||||
var node = CloneNode(segment.VdaNode);
|
||||
node.Released = true; // Base segments are always released
|
||||
nodes.Add(node);
|
||||
}
|
||||
|
||||
if (segment.VdaEdge != null)
|
||||
{
|
||||
var edge = CloneEdge(segment.VdaEdge);
|
||||
edge.Released = true; // Base segments are always released
|
||||
edges.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Horizon segments (released = false)
|
||||
foreach (var segment in route.Horizon)
|
||||
{
|
||||
if (segment.VdaNode != null)
|
||||
{
|
||||
var node = CloneNode(segment.VdaNode);
|
||||
node.Released = false; // Horizon segments are not released
|
||||
nodes.Add(node);
|
||||
}
|
||||
|
||||
if (segment.VdaEdge != null)
|
||||
{
|
||||
var edge = CloneEdge(segment.VdaEdge);
|
||||
edge.Released = false; // Horizon segments are not released
|
||||
edges.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
// Create initial Order
|
||||
var order = new OrderMsg
|
||||
{
|
||||
OrderId = route.OrderId,
|
||||
OrderUpdateId = 0,
|
||||
SerialNumber = robotId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Nodes = [.. nodes],
|
||||
Edges = [.. edges]
|
||||
};
|
||||
|
||||
var result = await robotController.SendOrderAsync(order);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
route.OrderUpdateId = order.OrderUpdateId;
|
||||
_logger.Info($"SendOrderToRobotAsync: Successfully sent initial Order (ID: {route.OrderId}, UpdateID: {order.OrderUpdateId}) to robot {robotId}");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning($"SendOrderToRobotAsync: Failed to send initial order to robot {robotId}: {result.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// OrderUpdate: Send stitching node + new Base segments + new Horizon segments
|
||||
// 1. Stitching node: last node of old Base (before release)
|
||||
// 2. New Base: segments that were just released from Horizon
|
||||
// 3. New Horizon: remaining segments in Horizon
|
||||
|
||||
if (baseCountBeforeRelease < 0)
|
||||
{
|
||||
// Fallback: use current Base.Count - 1 (assume only 1 segment was released)
|
||||
// This shouldn't happen if called from TryReleaseNextHorizonSegmentAsync
|
||||
baseCountBeforeRelease = Math.Max(0, route.Base.Count - 1);
|
||||
}
|
||||
|
||||
// Get stitching node: last node of Base before release
|
||||
var oldBaseSegments = route.Base.Take(baseCountBeforeRelease).ToList();
|
||||
var lastOldBaseNode = oldBaseSegments.LastOrDefault(s => s.VdaNode != null);
|
||||
if (lastOldBaseNode?.VdaNode == null)
|
||||
{
|
||||
_logger.Warning($"SendOrderToRobotAsync: Cannot create OrderUpdate for robot {robotId}: no old base node found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Add stitching node (last node of old Base, released = true)
|
||||
var stitchingNode = CloneNode(lastOldBaseNode.VdaNode);
|
||||
stitchingNode.Released = true;
|
||||
nodes.Add(stitchingNode);
|
||||
|
||||
// 2. Add new Base segments (segments that were just released, released = true)
|
||||
var newBaseSegments = route.Base.Skip(baseCountBeforeRelease).ToList();
|
||||
foreach (var segment in newBaseSegments)
|
||||
{
|
||||
if (segment.VdaNode != null)
|
||||
{
|
||||
var node = CloneNode(segment.VdaNode);
|
||||
node.Released = true; // New Base segments are released
|
||||
nodes.Add(node);
|
||||
}
|
||||
|
||||
if (segment.VdaEdge != null)
|
||||
{
|
||||
var edge = CloneEdge(segment.VdaEdge);
|
||||
edge.Released = true; // New Base segments are released
|
||||
edges.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Add new Horizon segments (remaining segments in Horizon, released = false)
|
||||
foreach (var segment in route.Horizon)
|
||||
{
|
||||
if (segment.VdaNode != null)
|
||||
{
|
||||
var node = CloneNode(segment.VdaNode);
|
||||
node.Released = false; // Horizon segments are not released
|
||||
nodes.Add(node);
|
||||
}
|
||||
|
||||
if (segment.VdaEdge != null)
|
||||
{
|
||||
var edge = CloneEdge(segment.VdaEdge);
|
||||
edge.Released = false; // Horizon segments are not released
|
||||
edges.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current order to increment OrderUpdateId
|
||||
var currentOrder = robotController.Data.Order;
|
||||
var orderUpdateId = currentOrder?.OrderUpdateId ?? 0;
|
||||
|
||||
var orderUpdate = new OrderMsg
|
||||
{
|
||||
OrderId = route.OrderId,
|
||||
OrderUpdateId = orderUpdateId + 1,
|
||||
SerialNumber = robotId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Nodes = [.. nodes],
|
||||
Edges = [.. edges]
|
||||
};
|
||||
|
||||
var result = await robotController.SendOrderAsync(orderUpdate);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
route.OrderUpdateId = orderUpdate.OrderUpdateId;
|
||||
_logger.Info($"SendOrderToRobotAsync: Successfully sent OrderUpdate (ID: {route.OrderId}, UpdateID: {orderUpdate.OrderUpdateId}) to robot {robotId}");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning($"SendOrderToRobotAsync: Failed to send OrderUpdate to robot {robotId}: {result.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"SendOrderToRobotAsync: Error sending order to robot {robotId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Node CloneNode(Node source)
|
||||
{
|
||||
return new Node
|
||||
{
|
||||
NodeId = source.NodeId,
|
||||
SequenceId = source.SequenceId,
|
||||
Released = source.Released,
|
||||
NodeDescription = source.NodeDescription,
|
||||
NodePosition = source.NodePosition is null ? null : new NodePosition
|
||||
{
|
||||
X = source.NodePosition.X,
|
||||
Y = source.NodePosition.Y,
|
||||
Theta = source.NodePosition.Theta,
|
||||
AllowedDeviationXY = source.NodePosition.AllowedDeviationXY,
|
||||
AllowedDeviationTheta = source.NodePosition.AllowedDeviationTheta,
|
||||
MapId = source.NodePosition.MapId,
|
||||
MapDescription = source.NodePosition.MapDescription
|
||||
},
|
||||
Actions = source.Actions?.Select(a => CloneAction(a)).ToArray() ?? [] // Clone each action
|
||||
};
|
||||
}
|
||||
|
||||
private static RobotNet.VDA5050.InstantAction.Action CloneAction(RobotNet.VDA5050.InstantAction.Action source)
|
||||
{
|
||||
return new RobotNet.VDA5050.InstantAction.Action
|
||||
{
|
||||
ActionType = source.ActionType,
|
||||
ActionId = source.ActionId,
|
||||
ActionDescription = source.ActionDescription,
|
||||
BlockingType = source.BlockingType,
|
||||
ActionParameters = source.ActionParameters?.Select(p => new RobotNet.VDA5050.InstantAction.ActionParameter
|
||||
{
|
||||
Key = p.Key,
|
||||
Value = p.Value
|
||||
}).ToArray() ?? null
|
||||
};
|
||||
}
|
||||
|
||||
private static Edge CloneEdge(Edge source)
|
||||
{
|
||||
return new Edge
|
||||
{
|
||||
EdgeId = source.EdgeId,
|
||||
SequenceId = source.SequenceId,
|
||||
Released = source.Released,
|
||||
EdgeDescription = source.EdgeDescription,
|
||||
StartNodeId = source.StartNodeId,
|
||||
EndNodeId = source.EndNodeId,
|
||||
MaxSpeed = source.MaxSpeed,
|
||||
MaxHeight = source.MaxHeight,
|
||||
MinHeight = source.MinHeight,
|
||||
Orientation = source.Orientation,
|
||||
OrientationType = source.OrientationType,
|
||||
Direction = source.Direction,
|
||||
RotationAllowed = source.RotationAllowed,
|
||||
MaxRotationSpeed = source.MaxRotationSpeed,
|
||||
Length = source.Length,
|
||||
Trajectory = source.Trajectory == null ? null : new Trajectory
|
||||
{
|
||||
Degree = source.Trajectory.Degree,
|
||||
KnotVector = [.. source.Trajectory.KnotVector], // Clone array
|
||||
ControlPoints = [.. source.Trajectory.ControlPoints.Select(cp => new ControlPoint
|
||||
{
|
||||
X = cp.X,
|
||||
Y = cp.Y,
|
||||
Weight = cp.Weight
|
||||
})]
|
||||
},
|
||||
Corridor = source.Corridor == null ? null : new Corridor
|
||||
{
|
||||
LeftWidth = source.Corridor.LeftWidth,
|
||||
RightWidth = source.Corridor.RightWidth,
|
||||
CorridorRefPoint = source.Corridor.CorridorRefPoint
|
||||
},
|
||||
Actions = source.Actions?.Select(a => CloneAction(a)).ToArray() ?? [] // Clone each action
|
||||
};
|
||||
}
|
||||
|
||||
private void CheckOrderCompletion(string robotId, OrderACSState orderState, StateMsg stateMsg)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if NodeStates and EdgeStates are empty
|
||||
var nodeStatesEmpty = stateMsg.NodeStates == null || stateMsg.NodeStates.Length == 0;
|
||||
var edgeStatesEmpty = stateMsg.EdgeStates == null || stateMsg.EdgeStates.Length == 0;
|
||||
|
||||
if (!nodeStatesEmpty || !edgeStatesEmpty)
|
||||
{
|
||||
// Still processing, not completed yet
|
||||
return;
|
||||
}
|
||||
|
||||
// NodeStates and EdgeStates are empty - check completion conditions
|
||||
var lastNodeInRoute = orderState.Route.FullRoute.LastOrDefault()?.VdaNode?.NodeId;
|
||||
var lastNodeId = stateMsg.LastNodeId;
|
||||
|
||||
// Check if LastNodeId matches the last node in route
|
||||
bool isAtLastNode = lastNodeId == lastNodeInRoute;
|
||||
|
||||
// Check actions on last node - get last node's actions from route
|
||||
bool allActionsFinished = true;
|
||||
bool hasActionFailed = false;
|
||||
|
||||
if (stateMsg.ActionStates != null && stateMsg.ActionStates.Length > 0)
|
||||
{
|
||||
// Get actions from last node in route
|
||||
var lastNodeSegment = orderState.Route.FullRoute.LastOrDefault(s => s.VdaNode != null);
|
||||
if (lastNodeSegment?.VdaNode?.Actions != null && lastNodeSegment.VdaNode.Actions.Length > 0)
|
||||
{
|
||||
// Check if all actions from last node are finished
|
||||
var lastNodeActionIds = lastNodeSegment.VdaNode.Actions.Select(a => a.ActionId).ToHashSet();
|
||||
var lastNodeActionStates = stateMsg.ActionStates
|
||||
.Where(a => lastNodeActionIds.Contains(a.ActionId))
|
||||
.ToList();
|
||||
|
||||
if (lastNodeActionStates.Count > 0)
|
||||
{
|
||||
foreach (var actionState in lastNodeActionStates)
|
||||
{
|
||||
if (actionState.ActionStatus == RobotNet.VDA5050.Type.ActionStatus.FAILED)
|
||||
{
|
||||
hasActionFailed = true;
|
||||
allActionsFinished = false;
|
||||
break;
|
||||
}
|
||||
else if (actionState.ActionStatus != RobotNet.VDA5050.Type.ActionStatus.FINISHED)
|
||||
{
|
||||
allActionsFinished = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine order status
|
||||
if (isAtLastNode && allActionsFinished)
|
||||
{
|
||||
if (orderState.Status != OrderStatus.IsCompleted) _logger.Info($"CheckOrderCompletion: Order completed successfully for robot {robotId}");
|
||||
// Order completed successfully
|
||||
orderState.Status = OrderStatus.IsCompleted;
|
||||
orderState.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
else if (!isAtLastNode || hasActionFailed)
|
||||
{
|
||||
// Order error: not at last node or action failed
|
||||
orderState.Status = OrderStatus.IsError;
|
||||
if (!isAtLastNode)
|
||||
{
|
||||
orderState.Error = $"Robot {robotId} finished at node {lastNodeId} but expected last node {lastNodeInRoute}";
|
||||
}
|
||||
else if (hasActionFailed)
|
||||
{
|
||||
orderState.Error = $"Robot {robotId} has failed actions on last node {lastNodeId}";
|
||||
}
|
||||
orderState.LastUpdated = DateTime.UtcNow;
|
||||
_logger.Warning($"CheckOrderCompletion: Order error for robot {robotId}: {orderState.Error}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"CheckOrderCompletion: Error for robot {robotId}: {ex.Message}");
|
||||
orderState.Status = OrderStatus.IsError;
|
||||
orderState.Error = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
public RobotRoute? GetRobotRoute(string robotId)
|
||||
{
|
||||
if (_orderStates.TryGetValue(robotId, out var state))
|
||||
{
|
||||
return state.Route;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace RobotNet10.FleetManager.Services.TrafficControl.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Severity levels for conflicts
|
||||
/// </summary>
|
||||
public enum ConflictSeverity
|
||||
{
|
||||
/// <summary>
|
||||
/// Low severity - can be resolved by waiting
|
||||
/// </summary>
|
||||
Low,
|
||||
|
||||
/// <summary>
|
||||
/// Medium severity - needs route adjustment
|
||||
/// </summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// High severity - needs complete reroute
|
||||
/// </summary>
|
||||
High
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace RobotNet10.FleetManager.Services.TrafficControl.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Types of conflicts between robots
|
||||
/// </summary>
|
||||
public enum ConflictType
|
||||
{
|
||||
/// <summary>
|
||||
/// Xảy ra khi hai robot sử dụng cùng một cạnh (edge) trong biểu đồ đường đi (graph)
|
||||
/// trong khoảng thời gian trùng lặp, đồng thời lộ trình tiếp theo của robot chồng lên nhau.
|
||||
/// </summary>
|
||||
Confrontation,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra khi hai robot sử dụng cùng một cạnh (edge) trong biểu đồ đường đi (graph)
|
||||
/// trong khoảng thời gian trùng lặp nhưng lộ trình tiếp theo của 2 robot không chồng lấn lên nhau.
|
||||
/// </summary>
|
||||
Edge,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra khi hai robot chiếm cùng một nút (vertex/node) trong biểu đồ đường đi
|
||||
/// tại cùng một thời điểm hoặc trong khoảng thời gian trùng lặp.
|
||||
/// </summary>
|
||||
Vertex,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra khi hai robot ở quá gần nhau (dựa trên khoảng cách Euclidean) trong không gian liên tục,
|
||||
/// vi phạm khoảng cách an toàn (minDistance).
|
||||
/// </summary>
|
||||
Proximity,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra khi hai robot di chuyển qua một hành lang hẹp (thường được biểu diễn bằng một chuỗi cạnh hoặc node)
|
||||
/// theo hướng ngược nhau, dẫn đến tình trạng không thể vượt qua nhau.
|
||||
/// </summary>
|
||||
Corridor,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra khi hai robot có lộ trình giao nhau về mặt thời gian, nhưng không nhất thiết ở cùng một cạnh hoặc nút,
|
||||
/// mà ở các vị trí khiến chúng không thể di chuyển tiếp mà không va chạm.
|
||||
/// </summary>
|
||||
Temporal,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra khi hai robot cần xoay tại một điểm (thường là node) và không gian xoay bị chồng lấn,
|
||||
/// dẫn đến va chạm hoặc cản trở.
|
||||
/// </summary>
|
||||
Rotation,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra khi các robot cạnh tranh cho một tài nguyên chung (ví dụ: một khu vực làm việc, điểm sạc, hoặc thiết bị nâng)
|
||||
/// </summary>
|
||||
Resource,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra lỗi khi kiểm tra xung đột
|
||||
/// </summary>
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user