62 Commits

Author SHA1 Message Date
Đăng Nguyễn
b7e7d70855 update 2025-12-31 14:59:56 +07:00
Đăng Nguyễn
5c1851e92f update 2025-12-31 14:03:47 +07:00
Đăng Nguyễn
49c0c1ab39 update 2025-12-31 13:25:10 +07:00
Đăng Nguyễn
8362713dcc update 2025-12-31 08:56:53 +07:00
Đăng Nguyễn
15a61fd986 update 2025-12-30 17:38:43 +07:00
Đăng Nguyễn
b3f765d261 update 2025-12-30 16:59:08 +07:00
Đăng Nguyễn
2785a8f161 update 2025-12-30 15:17:42 +07:00
Đăng Nguyễn
a51cfe80c8 update 2025-12-23 09:52:42 +07:00
Đăng Nguyễn
e4e135e35f update 2025-12-22 21:28:57 +07:00
Đăng Nguyễn
30732b4b9f udpate robot control 2025-12-22 20:25:22 +07:00
Đăng Nguyễn
b2eeb8cb3f update 2025-12-22 19:49:03 +07:00
7ce770404c save 2025-12-22 19:45:25 +07:00
Đăng Nguyễn
128600c4ed update 2025-12-22 19:44:08 +07:00
Đăng Nguyễn
b006c5b197 update console 2025-12-22 19:43:04 +07:00
Đăng Nguyễn
4ceec9abd5 update 2025-12-22 19:31:55 +07:00
Đăng Nguyễn
1289a6c331 update 2025-12-22 18:39:38 +07:00
Đăng Nguyễn
f1a7be15f2 update 2025-12-22 18:38:35 +07:00
d2cf86f34e update 2025-12-22 18:38:10 +07:00
Đăng Nguyễn
d4af3b8707 Merge remote-tracking branch 'origin/sonlt' into dangnv 2025-12-22 18:12:25 +07:00
909e147be1 save 2025-12-22 18:08:02 +07:00
dc837e5488 save 2025-12-22 17:48:48 +07:00
Đăng Nguyễn
7daad2dfaf update 2025-12-22 16:23:26 +07:00
Đăng Nguyễn
38858355e6 Merge remote-tracking branch 'origin/sonlt' into dangnv 2025-12-22 16:11:45 +07:00
Đăng Nguyễn
65217021d4 update 2025-12-22 16:11:29 +07:00
7a6f813825 save 2025-12-22 14:35:55 +07:00
Đăng Nguyễn
92c01004f5 update 2025-12-22 10:33:24 +07:00
7e0c6af9d5 save 2025-12-22 09:49:02 +07:00
f6f8a3bf65 save 2025-12-22 09:48:17 +07:00
Đăng Nguyễn
a8296063f5 update logs 2025-12-22 09:43:00 +07:00
f6a69d1673 save 2025-12-21 16:50:37 +07:00
45082a98cd save 2025-12-21 16:32:06 +07:00
93599f5c95 save 2025-12-21 11:33:11 +07:00
f52f0fd8da save 2025-12-20 18:01:54 +07:00
3e7bcd82b6 save 2025-12-20 17:57:54 +07:00
Đăng Nguyễn
bc00e9ae50 update 2025-12-20 17:50:47 +07:00
Đăng Nguyễn
062a6478ce update 2025-12-20 17:37:22 +07:00
Đăng Nguyễn
6cd32f8c98 update 2025-12-20 15:16:59 +07:00
Đăng Nguyễn
dd8c17cb6c update 2025-12-17 15:34:15 +07:00
Đăng Nguyễn
c35da9a73f update with speed 2.0 m/s 2025-11-13 10:02:04 +07:00
Đăng Nguyễn
3b088a6d5d update connect synaos 2025-11-12 14:03:04 +07:00
Đăng Nguyễn
8736bad3e7 update 2025-11-06 14:14:10 +07:00
Đăng Nguyễn
99716cc414 update 2025-11-06 09:22:55 +07:00
Đăng Nguyễn
73038de662 update 2025-11-04 10:57:41 +07:00
Đăng Nguyễn
70e27da4a2 update 2025-11-03 10:29:18 +07:00
Đăng Nguyễn
aea55d52f1 update 2025-10-31 15:03:37 +07:00
Đăng Nguyễn
aa2146e383 update 2025-10-30 13:34:44 +07:00
Đăng Nguyễn
643a34a4b4 update 2025-10-28 17:28:46 +07:00
Đăng Nguyễn
6eeed8c7b4 update 2025-10-24 17:09:00 +07:00
Đăng Nguyễn
a01f140f2e update 2025-10-24 10:24:59 +07:00
Đăng Nguyễn
ab5d3e1a1a update 2025-10-22 11:16:19 +07:00
Đăng Nguyễn
9ac5270885 update 2025-10-17 09:24:45 +07:00
Đăng Nguyễn
90dcb67b60 update 2025-10-16 14:53:22 +07:00
Đăng Nguyễn
b2df5b22b7 update 2025-10-13 13:17:32 +07:00
Đăng Nguyễn
511614df72 update 2025-10-03 11:37:39 +07:00
Đăng Nguyễn
c5686e4ecf update 2025-10-03 11:31:14 +07:00
Đăng Nguyễn
2853340856 draw images map 2025-10-02 17:28:04 +07:00
Đăng Nguyễn
811a5821ba update mouse indicator 2025-10-02 14:16:20 +07:00
Đăng Nguyễn
3b44ea6d8d update ruller 2025-10-02 11:02:44 +07:00
Đăng Nguyễn
93097412b0 update move. scale mapping 2025-10-02 09:49:16 +07:00
Đăng Nguyễn
2640fb92c1 update 2025-09-27 14:47:42 +07:00
Đăng Nguyễn
2bbcc19076 merge 2025-09-26 13:41:14 +07:00
Đăng Nguyễn
4aed0da992 update mapmanager pages 2025-09-25 21:03:10 +07:00
218 changed files with 17591 additions and 2758 deletions

66
.dockerignore Normal file
View File

@@ -0,0 +1,66 @@
# Build artifacts
**/bin/
**/obj/
**/out/
# Visual Studio files
**/.vs/
**/.vscode/
**/*.user
**/*.suo
**/*.userosscache
**/*.sln.docstates
# User-specific files
**/.user
**/.suo
**/.userosscache
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# NuGet packages
**/packages/
**/*.nupkg
**/*.snupkg
# Test results
**/[Tt]est[Rr]esult*/
**/[Bb]uild[Ll]og.*
# Docker files
Dockerfile*
docker-compose*
.dockerignore
# Git
.git/
.gitignore
.gitattributes
# IDE
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Data and logs (will be mounted as volumes)
data/
logs/

57
Dockerfile Normal file
View File

@@ -0,0 +1,57 @@
# Stage 1: Build
# Note: Project files specify net10.0, but using .NET 9.0 based on package versions (9.0.9)
# Adjust version if needed: 8.0 (LTS), 9.0 (current), or future 10.0
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy solution file
COPY RobotApp.sln .
# Copy project files
COPY RobotApp/RobotApp.csproj RobotApp/
COPY RobotApp.Client/RobotApp.Client.csproj RobotApp.Client/
COPY RobotApp.Common.Shares/RobotApp.Common.Shares.csproj RobotApp.Common.Shares/
COPY RobotApp.VDA5050/RobotApp.VDA5050.csproj RobotApp.VDA5050/
# Restore dependencies
RUN dotnet restore RobotApp.sln
# Copy all source files
COPY RobotApp/ RobotApp/
COPY RobotApp.Client/ RobotApp.Client/
COPY RobotApp.Common.Shares/ RobotApp.Common.Shares/
COPY RobotApp.VDA5050/ RobotApp.VDA5050/
RUN rm -rf ./RobotApp/RobotApp/bin
RUN rm -rf ./RobotApp/RobotApp/obj
RUN rm -rf ./RobotApp.Client/RobotApp.Client/bin
RUN rm -rf ./RobotApp.Client/RobotApp.Client/obj
RUN rm -rf ./RobotApp.Common.Shares/RobotApp.Common.Shares/bin
RUN rm -rf ./RobotApp.Common.Shares/RobotApp.Common.Shares/obj
RUN rm -rf ./RobotApp.VDA5050/RobotApp.VDA5050/bin
RUN rm -rf ./RobotApp.VDA5050/RobotApp.VDA5050/obj
# Build the solution
WORKDIR /src/RobotApp
RUN dotnet build -c Release -o /app/build
# Stage 2: Publish
FROM build AS publish
WORKDIR /src/RobotApp
RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false
# Copy published files
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish ./
# Set environment variables
#ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
# Run the application
ENTRYPOINT ["dotnet", "RobotApp.dll"]

View File

@@ -1,2 +1,5 @@
# RobotApp # RobotApp
docker build -t robotics.doc/robotnet/robotapp_dde:2.7 .
docker save -o robotapp-dde.2.7.tar robotics.doc/robotnet/robotapp_dde:2.7
scp .\robotapp-dde.2.7.tar robotics@172.20.235.176:~/DDE

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components.Web;
namespace RobotApp.Client;
public class ClientRenderMode
{
public static InteractiveWebAssemblyRenderMode InteractiveWebAssemblyNoPrerender { get; } = new(prerender: false);
}

View File

@@ -1,20 +0,0 @@
@inherits LayoutComponentBase
@using MudBlazor
<div class="app-shell">
<AuthorizeView>
<Authorized>
<NavMenu />
</Authorized>
</AuthorizeView>
<main class="page">
@Body
</main>
</div>
<MudPopoverProvider />
<MudThemeProvider IsDarkMode />
<MudDialogProvider MaxWidth="MaxWidth.ExtraLarge" />
<MudSnackbarProvider />

View File

@@ -1,17 +0,0 @@
.app-shell {
display: flex;
min-height: 100vh;
min-width: 100vw;
width: 100vw;
height: 100vh;
overflow: hidden;
flex-direction: row;
}
.page {
flex: 1 1 auto;
min-width: 0;
display: flex;
overflow: hidden;
}

View File

@@ -1,78 +0,0 @@
@using Microsoft.AspNetCore.Components.Routing
<script>
function toggleSidebar() {
let sidebar = document.querySelector(".sidebar");
sidebar.classList.toggle("collapsed");
}
</script>
<div class="sidebar collapsed">
<div class="flex-grow-1 d-flex flex-column">
<div class="title">
<img src="images/logoLight.svg" alt="PhenikaaX" style="height: 35px;" onclick="toggleSidebar()" />
<button class="btn button" onclick="toggleSidebar()">
<i class="mdi mdi-menu" style="color: white; font-size: 35px"></i>
</button>
</div>
<hr />
@foreach (var nav in Navs)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="@nav.Path" Match="@nav.Match">
<div class="d-flex align-items-center">
<div class="nav-icon">
<span class="mdi @nav.Icon mdi-36px" aria-hidden="true"></span>
</div>
<span class="nav-label">@nav.Label</span>
</div>
</NavLink>
</div>
}
</div>
<div class="user">
<div>
<span class="mdi mdi-account mdi-36px text-white "></span>
</div>
<AuthorizeView>
<Authorized>
<div class="nav-label">
<MudText Class="text-white name" Typo="Typo.subtitle1">@context.User.Identity?.Name</MudText>
</div>
</Authorized>
</AuthorizeView>
<MudSpacer />
<form action="Account/Logout" method="post">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="" />
<button class="btn button">
<i class="mdi mdi-logout" style="color: white; font-size: 35px"></i>
</button>
@* <MudIconButton Class="text-white" ButtonType="@ButtonType.Submit" Icon="@Icons.Material.Filled.Logout" /> *@
</form>
</div>
</div>
@code {
public class NavModel
{
public string Icon { get; set; } = "";
public string Path { get; set; } = "";
public string Label { get; set; } = "";
public NavLinkMatch Match { get; set; }
}
public NavModel[] Navs = [
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
new(){Icon = "mdi-map-legend", Path="/Maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
];
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -0,0 +1,89 @@
@inherits LayoutComponentBase
<script>
function toggleSidebar() {
let sidebar = document.querySelector(".sidebar");
sidebar.classList.toggle("collapsed");
}
</script>
<div class="app-shell">
<div class="sidebar collapsed">
<div class="flex-grow-1 d-flex flex-column">
<div class="title">
<img src="images/logoLight.svg" alt="PhenikaaX" style="height: 35px;" onclick="toggleSidebar()" />
<button class="btn button" onclick="toggleSidebar()">
<i class="mdi mdi-menu" style="color: white; font-size: 35px"></i>
</button>
</div>
<hr />
@foreach (var nav in Navs)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="@nav.Path" Match="@nav.Match">
<div class="d-flex align-items-center">
<div class="nav-icon">
<span class="mdi @nav.Icon mdi-36px" aria-hidden="true"></span>
</div>
<span class="nav-label">@nav.Label</span>
</div>
</NavLink>
</div>
}
</div>
<div class="user">
<div>
<span class="mdi mdi-account mdi-36px text-white "></span>
</div>
<AuthorizeView>
<Authorized>
<div class="nav-label">
<MudText Class="text-white" Typo="Typo.subtitle1">@context.User.Identity?.Name</MudText>
</div>
</Authorized>
</AuthorizeView>
<MudSpacer />
<form action="Account/Logout" method="post">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="" />
<button class="btn button">
<i class="mdi mdi-logout" style="color: white; font-size: 35px"></i>
</button>
@* <MudIconButton Class="text-white" ButtonType="@ButtonType.Submit" Icon="@Icons.Material.Filled.Logout" /> *@
</form>
</div>
</div>
<main class="page">
@Body
</main>
</div>
@code {
public class NavModel
{
public string Icon { get; set; } = "";
public string Path { get; set; } = "";
public string Label { get; set; } = "";
public NavLinkMatch Match { get; set; }
}
public NavModel[] Navs = [
new(){Icon = "mdi-view-dashboard", Path="/dashboard", Label = "Dashboard", Match = NavLinkMatch.All},
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All},
new(){Icon = "mdi-state-machine", Path="/robot-order", Label = "order", Match = NavLinkMatch.All},
new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All },
new(){Icon = "mdi-math-log", Path="/logs", Label = "Logs", Match = NavLinkMatch.All},
];
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -1,3 +1,21 @@
.app-shell {
display: flex;
min-height: 100vh;
min-width: 100vw;
width: 100vw;
height: 100vh;
overflow: hidden;
flex-direction: row;
}
.page {
flex: 1 1 auto;
min-width: 0;
display: flex;
overflow: hidden;
}
.sidebar { .sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);

View File

@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
namespace RobotApp.Client.Models;
public class LoggerModel
{
[JsonPropertyName("time")]
public string? Time { get; set; }
[JsonPropertyName("level")]
public string? Level { get; set; }
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("exception")]
public string? Exception { get; set; }
public string ColorClass => Level switch
{
"WARN" => "text-warning",
"INFO" => "text-info",
"DEBUG" => "text-success",
"ERROR" => "text-danger",
"FATAL" => "text-secondary",
_ => "text-muted",
};
public string BackgroundClass => Level switch
{
"WARN" => "bg-warning text-dark",
"INFO" => "bg-info text-dark",
"DEBUG" => "bg-success text-white",
"ERROR" => "bg-danger text-white",
"FATAL" => "bg-secondary text-white",
_ => "bg-dark text-white",
};
public bool HasException => !string.IsNullOrEmpty(Exception);
}

View File

@@ -0,0 +1,12 @@
@inherits LayoutComponentBase
@layout RobotApp.Client.MainLayout
<MudThemeProvider @rendermode="InteractiveWebAssemblyNoPrerender" IsDarkMode/>
<MudPopoverProvider @rendermode="InteractiveWebAssemblyNoPrerender" />
<MudDialogProvider @rendermode="InteractiveWebAssemblyNoPrerender" MaxWidth="MaxWidth.ExtraLarge"
CloseButton="false"
BackdropClick="false"
Position="DialogPosition.Center" />
<MudSnackbarProvider @rendermode="InteractiveWebAssemblyNoPrerender" />
@Body

View File

@@ -1,13 +0,0 @@
@page "/auth"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<PageTitle>Auth</PageTitle>
<h1>You are authenticated</h1>
<AuthorizeView>
Hello @context.User.Identity?.Name!
</AuthorizeView>

View File

@@ -0,0 +1,92 @@
@implements IDisposable
<div class ="d-flex w-100 h-100 flex-column">
<EditForm EditContext="EditContext">
<DataAnnotationsValidator />
<div class="mb-2">
<label class="form-label">Navigation Type</label>
<InputSelect class="form-select" @bind-Value="Model.NavigationType" TValue="NavigationType">
@foreach (var t in NavigationTypes)
{
<option value="@t">@t</option>
}
</InputSelect>
<ValidationMessage For="@(() => Model.NavigationType)" />
</div>
<div class="row g-2 mb-2">
<div class="col">
<label class="form-label">Radius (wheel)</label>
<InputNumber class="form-control" @bind-Value="Model.RadiusWheel" />
<ValidationMessage For="@(() => Model.RadiusWheel)" />
</div>
<div class="col">
<label class="form-label">Width (m)</label>
<InputNumber class="form-control" @bind-Value="Model.Width" />
<ValidationMessage For="@(() => Model.Width)" />
</div>
</div>
<div class="row g-2 mb-2">
<div class="col">
<label class="form-label">Length (m)</label>
<InputNumber class="form-control" @bind-Value="Model.Length" />
<ValidationMessage For="@(() => Model.Length)" />
</div>
<div class="col">
<label class="form-label">Height (m)</label>
<InputNumber class="form-control" @bind-Value="Model.Height" />
<ValidationMessage For="@(() => Model.Height)" />
</div>
</div>
<div class="mb-2">
<label class="form-label">Description</label>
<InputTextArea class="form-control" @bind-Value="Model.Description" />
<ValidationMessage For="@(() => Model.Description)" />
</div>
</EditForm>
<div class="flex-grow-1" />
<div>
@if (Model.CreatedAt != default || Model.UpdatedAt != default)
{
<div class="d-flex justify-content-end mt-2">
<small class="text-muted">Created: @Model.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
<small class="text-muted ms-3">Updated: @Model.UpdatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
</div>
}
</div>
</div>
@code {
[Parameter]
public RobotConfigDto Model { get; set; } = new();
[Parameter]
public EventCallback<RobotConfigDto> ModelChanged { get; set; }
private EditContext? EditContext;
private IEnumerable<NavigationType> NavigationTypes => Enum.GetValues(typeof(NavigationType)).Cast<NavigationType>();
protected override void OnParametersSet()
{
if (EditContext is null || !EditContext.Model!.Equals(Model))
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
EditContext = new EditContext(Model);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
}
}
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
_ = ModelChanged.InvokeAsync(Model);
}
public void Dispose()
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
}
}

View File

@@ -0,0 +1,74 @@
@implements IDisposable
<div class="d-flex w-100 h-100 flex-column">
<EditForm EditContext="EditContext">
<DataAnnotationsValidator />
<div class="row g-2 mb-2">
<div class="col-md-8">
<label class="form-label">PLC Address</label>
<InputText class="form-control" @bind-Value="Model.PLCAddress" />
<ValidationMessage For="@(() => Model.PLCAddress)" />
</div>
<div class="col-md-4">
<label class="form-label">Port</label>
<InputNumber class="form-control" @bind-Value="Model.PLCPort" />
<ValidationMessage For="@(() => Model.PLCPort)" />
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-md-4">
<label class="form-label">Unit Id</label>
<InputNumber class="form-control" @bind-Value="Model.PLCUnitId" />
<ValidationMessage For="@(() => Model.PLCUnitId)" />
</div>
</div>
<div class="mb-2">
<label class="form-label">Description</label>
<InputTextArea class="form-control" @bind-Value="Model.Description" />
<ValidationMessage For="@(() => Model.Description)" />
</div>
</EditForm>
<div class="flex-grow-1" />
<div>
@if (Model.CreatedAt != default || Model.UpdatedAt != default)
{
<div class="d-flex justify-content-end mt-2">
<small class="text-muted">Created: @Model.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
<small class="text-muted ms-3">Updated: @Model.UpdatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
</div>
}
</div>
</div>
@code {
[Parameter]
public RobotPlcConfigDto Model { get; set; } = new();
[Parameter]
public EventCallback<RobotPlcConfigDto> ModelChanged { get; set; }
private EditContext? EditContext;
protected override void OnParametersSet()
{
if (EditContext is null || !EditContext.Model!.Equals(Model))
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
EditContext = new EditContext(Model);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
}
}
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
_ = ModelChanged.InvokeAsync(Model);
}
public void Dispose()
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
}
}

View File

@@ -0,0 +1,97 @@
@implements IDisposable
<div class="d-flex w-100 h-100 flex-column">
<EditForm EditContext="EditContext">
<DataAnnotationsValidator />
<div class="row g-2 mb-2">
<div class="col-6">
<label class="form-label">Very Slow (m/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SafetySpeedVerySlow" />
<ValidationMessage For="@(() => Model.SafetySpeedVerySlow)" />
</div>
<div class="col-6">
<label class="form-label">Slow (m/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SafetySpeedSlow" />
<ValidationMessage For="@(() => Model.SafetySpeedSlow)" />
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-6">
<label class="form-label">Normal (m/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SafetySpeedNormal" />
<ValidationMessage For="@(() => Model.SafetySpeedNormal)" />
</div>
<div class="col-6">
<label class="form-label">Medium (m/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SafetySpeedMedium" />
<ValidationMessage For="@(() => Model.SafetySpeedMedium)" />
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-6">
<label class="form-label">Optimal (m/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SafetySpeedOptimal" />
<ValidationMessage For="@(() => Model.SafetySpeedOptimal)" />
</div>
<div class="col-6">
<label class="form-label">Fast (m/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SafetySpeedFast" />
<ValidationMessage For="@(() => Model.SafetySpeedFast)" />
</div>
</div>
<div class="mb-2">
<label class="form-label">Very Fast (m/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SafetySpeedVeryFast" />
<ValidationMessage For="@(() => Model.SafetySpeedVeryFast)" />
</div>
<div class="mb-2">
<label class="form-label">Description</label>
<InputTextArea class="form-control" @bind-Value="Model.Description" />
<ValidationMessage For="@(() => Model.Description)" />
</div>
</EditForm>
<div class="flex-grow-1" />
<div>
@if (Model.CreatedAt != default || Model.UpdatedAt != default)
{
<div class="d-flex justify-content-end mt-2">
<small class="text-muted">Created: @Model.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
<small class="text-muted ms-3">Updated: @Model.UpdatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
</div>
}
</div>
</div>
@code {
[Parameter]
public RobotSafetyConfigDto Model { get; set; } = new();
[Parameter]
public EventCallback<RobotSafetyConfigDto> ModelChanged { get; set; }
private EditContext? EditContext;
protected override void OnParametersSet()
{
if (EditContext is null || !EditContext.Model!.Equals(Model))
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
EditContext = new EditContext(Model);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
}
}
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
_ = ModelChanged.InvokeAsync(Model);
}
public void Dispose()
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
}
}

View File

@@ -0,0 +1,85 @@
@implements IDisposable
<div class="d-flex w-100 h-100 flex-column">
<EditForm EditContext="EditContext">
<DataAnnotationsValidator />
<div class="form-check mb-2">
<InputCheckbox class="form-check-input" Value="Model.EnableSimulation" />
<label class="form-check-label">Enable Simulation</label>
<ValidationMessage For="@(() => Model.EnableSimulation)" />
</div>
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label">Max Velocity (m/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SimulationMaxVelocity" />
<ValidationMessage For="@(() => Model.SimulationMaxVelocity)" />
</div>
<div class="col-md-6">
<label class="form-label">Max Angular Velocity (rad/s)</label>
<InputNumber class="form-control" @bind-Value="Model.SimulationMaxAngularVelocity" />
<ValidationMessage For="@(() => Model.SimulationMaxAngularVelocity)" />
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label">Acceleration (m/s²)</label>
<InputNumber class="form-control" @bind-Value="Model.SimulationAcceleration" />
<ValidationMessage For="@(() => Model.SimulationAcceleration)" />
</div>
<div class="col-md-6">
<label class="form-label">Deceleration (m/s²)</label>
<InputNumber class="form-control" @bind-Value="Model.SimulationDeceleration" />
<ValidationMessage For="@(() => Model.SimulationDeceleration)" />
</div>
</div>
<div class="mb-2">
<label class="form-label">Description</label>
<InputTextArea class="form-control" @bind-Value="Model.Description" />
<ValidationMessage For="@(() => Model.Description)" />
</div>
</EditForm>
<div class="flex-grow-1" />
<div>
@if (Model.CreatedAt != default || Model.UpdatedAt != default)
{
<div class="d-flex justify-content-end mt-2">
<small class="text-muted">Created: @Model.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
<small class="text-muted ms-3">Updated: @Model.UpdatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
</div>
}
</div>
</div>
@code {
[Parameter]
public RobotSimulationConfigDto Model { get; set; } = new();
[Parameter]
public EventCallback<RobotSimulationConfigDto> ModelChanged { get; set; }
private EditContext? EditContext;
protected override void OnParametersSet()
{
if (EditContext is null || !EditContext.Model!.Equals(Model))
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
EditContext = new EditContext(Model);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
}
}
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
_ = ModelChanged.InvokeAsync(Model);
}
public void Dispose()
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
}
}

View File

@@ -0,0 +1,329 @@
@implements IDisposable
<div class="d-flex w-100 h-100 flex-column">
<EditForm EditContext="EditContext">
<DataAnnotationsValidator />
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label" for="serialNumber">Serial Number</label>
<InputText id="serialNumber" class="form-control" @bind-Value="Model.SerialNumber" />
<ValidationMessage For="@(() => Model.SerialNumber)" />
</div>
<div class="col-md-6">
<label class="form-label" for="prefix">Topic Prefix</label>
<InputText id="prefix" class="form-control" @bind-Value="Model.VDA5050TopicPrefix"/>
<ValidationMessage For="@(() => Model.VDA5050TopicPrefix)" />
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label" for="manufacturer">Manufacturer</label>
<InputText id="manufacturer" class="form-control" @bind-Value="Model.VDA5050Manufacturer" disabled="true" />
<ValidationMessage For="@(() => Model.VDA5050Manufacturer)" />
</div>
<div class="col-md-6">
<label class="form-label" for="version">Version</label>
<InputText id="version" class="form-control" @bind-Value="Model.VDA5050Version" disabled="true" />
<ValidationMessage For="@(() => Model.VDA5050Version)" />
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label" for="host">Host</label>
<InputText id="host" class="form-control" @bind-Value="Model.VDA5050HostServer" />
<ValidationMessage For="@(() => Model.VDA5050HostServer)" />
</div>
<div class="col-md-3">
<label class="form-label" for="port">Port</label>
<InputNumber id="port" class="form-control" @bind-Value="Model.VDA5050Port" />
<ValidationMessage For="@(() => Model.VDA5050Port)" />
</div>
<div class="col-md-3">
<label class="form-label" for="publishRepeat">Publish Repeat</label>
<InputNumber id="publishRepeat" class="form-control" @bind-Value="Model.VDA5050PublishRepeat" />
<ValidationMessage For="@(() => Model.VDA5050PublishRepeat)" />
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-md-6">
<label class="form-label" for="username">Username</label>
<InputText id="username" class="form-control" @bind-Value="Model.VDA5050UserName" />
<ValidationMessage For="@(() => Model.VDA5050UserName)" />
</div>
<div class="col-md-6">
<label class="form-label" for="password">Password</label>
<div class="password-input-wrapper">
<InputText id="password" class="form-control password-input" @bind-Value="Model.VDA5050Password" inputmode="text" type="@PasswordInputType" autocomplete="new-password" spellcheck="false" />
<button class="password-toggle-btn" type="button" @onclick="TogglePasswordVisibility" aria-label="Toggle password visibility">
<i class="@PasswordIconClass" aria-hidden="true"></i>
</button>
</div>
<ValidationMessage For="@(() => Model.VDA5050Password)" />
</div>
</div>
<div class="form-check mb-2">
<InputCheckbox id="enablePassword" class="form-check-input" @bind-Value="Model.VDA5050EnablePassword" />
<label class="form-check-label" for="enablePassword">Enable Password</label>
<ValidationMessage For="@(() => Model.VDA5050EnablePassword)" />
</div>
<div class="form-check mb-2">
<InputCheckbox id="enableTls" class="form-check-input" @bind-Value="Model.VDA5050EnableTls" />
<label class="form-check-label" for="enableTls">Enable TLS</label>
<ValidationMessage For="@(() => Model.VDA5050EnableTls)" />
</div>
<div class="mb-2">
<label class="form-label" for="caFile">CA File</label>
<div class="custom-file-input-wrapper position-relative">
<InputFile class="d-none" id="caFile" OnChange="e => OnFileSelected(e, FileSlot.Ca)" accept=".crt,.pem,.cer,.pfx,.key,.jks" />
<div class="form-control d-flex align-items-center gap-2 ps-0">
<label for="caFile" class="upload-btn d-flex align-items-center gap-1">
<i class="mdi mdi-attachment"></i>
</label>
<span id="fileNameDisplay" class="text-muted flex-grow-1 text-truncate" style="max-width: 200px;">
@Model.VDA5050CA
</span>
<button class="password-toggle-btn position-absolute rounded-end-2 top-50 translate-middle-y" type="button" @onclick="() => RemoveFile(FileSlot.Ca)" aria-label="Remove client certificate file">
<i class="mdi mdi-close" aria-hidden="true"></i>
</button>
</div>
</div>
@if (!string.IsNullOrEmpty(CaFileInfo))
{
<div class="small text-muted mt-1 mb-1">@CaFileInfo</div>
}
@if (!string.IsNullOrEmpty(CaFileError))
{
<div class="text-danger small mt-1 mb-1">@CaFileError</div>
}
</div>
<div class="mb-2">
<label class="form-label" for="clientCertFile">Client Certificate File</label>
<div class="custom-file-input-wrapper position-relative">
<InputFile class="d-none" id="clientCertFile" OnChange="e => OnFileSelected(e, FileSlot.Cert)" accept=".crt,.pem,.cer,.pfx,.key,.jks" />
<div class="form-control d-flex align-items-center gap-2 ps-0">
<label for="clientCertFile" class="upload-btn d-flex align-items-center gap-1">
<i class="mdi mdi-attachment"></i>
</label>
<span id="fileNameDisplay" class="text-muted flex-grow-1 text-truncate" style="max-width: 200px;">
@Model.VDA5050Cer
</span>
<button class="password-toggle-btn position-absolute rounded-end-2 top-50 translate-middle-y" type="button" @onclick="() => RemoveFile(FileSlot.Cert)" aria-label="Remove client certificate file">
<i class="mdi mdi-close" aria-hidden="true"></i>
</button>
</div>
</div>
@if (!string.IsNullOrEmpty(CertFileInfo))
{
<div class="small text-muted mt-1 mb-1">@CertFileInfo</div>
}
@if (!string.IsNullOrEmpty(CertFileError))
{
<div class="text-danger small mt-1 mb-1">@CertFileError</div>
}
</div>
<div class="mb-2">
<label class="form-label" for="clientKeyFile">Client Key File</label>
<div class="custom-file-input-wrapper position-relative">
<InputFile class="d-none" id="clientKeyFile" OnChange="e => OnFileSelected(e, FileSlot.Key)" accept=".crt,.pem,.cer,.pfx,.key,.jks" />
<div class="form-control d-flex align-items-center gap-2 ps-0">
<label for="clientKeyFile" class="upload-btn d-flex align-items-center gap-1">
<i class="mdi mdi-attachment"></i>
</label>
<span id="fileNameDisplay" class="text-muted flex-grow-1 text-truncate" style="max-width: 200px;">
@Model.VDA5050Key
</span>
<button class="password-toggle-btn position-absolute rounded-end-2 top-50 translate-middle-y" type="button" @onclick="() => RemoveFile(FileSlot.Key)" aria-label="Remove client key file">
<i class="mdi mdi-close" aria-hidden="true"></i>
</button>
</div>
</div>
@if (!string.IsNullOrEmpty(KeyFileInfo))
{
<div class="small text-muted mt-1 mb-1">@KeyFileInfo</div>
}
@if (!string.IsNullOrEmpty(KeyFileError))
{
<div class="text-danger small mt-1 mb-1">@KeyFileError</div>
}
</div>
<div class="mb-2">
<label class="form-label" for="description">Description</label>
<InputTextArea id="description m-1" class="form-control" @bind-Value="Model.Description" />
<ValidationMessage For="@(() => Model.Description)" />
</div>
</EditForm>
<div class="flex-grow-1" />
<div>
@if (Model.CreatedAt != default || Model.UpdatedAt != default)
{
<div class="d-flex justify-content-end mt-2">
<small class="text-muted">Created: @Model.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
<small class="text-muted ms-3">Updated: @Model.UpdatedAt.ToString("dd/MM/yyyy HH:mm:ss")</small>
</div>
}
</div>
</div>
@code {
[Parameter]
public RobotVDA5050ConfigDto Model { get; set; } = new();
[Parameter]
public EventCallback<RobotVDA5050ConfigDto> ModelChanged { get; set; }
public IBrowserFile? CaFile { get; set; }
public IBrowserFile? CertFile { get; set; }
public IBrowserFile? KeyFile { get; set; }
public long MaxFileSize { get; set; } = 10 * 1024 * 1024;
private EditContext? EditContext;
private bool showPassword;
private string PasswordInputType => showPassword ? "text" : "password";
private string PasswordIconClass => showPassword ? "mdi mdi-eye-off" : "mdi mdi-eye";
private enum FileSlot { Ca, Cert, Key }
private string? CaFileInfo;
private string? CaFileError;
private string? CertFileInfo;
private string? CertFileError;
private string? KeyFileInfo;
private string? KeyFileError;
private async Task OnFileSelected(InputFileChangeEventArgs e, FileSlot slot)
{
var file = e.File;
if (file is null)
return;
if (file.Size > MaxFileSize)
{
SetFileError(slot, $"File too large (max {FormatSize(MaxFileSize)})");
SetFileInfo(slot, string.Empty, 0);
return;
}
try
{
using var stream = file.OpenReadStream(MaxFileSize);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var data = ms.ToArray();
SetFileError(slot, null);
SetFileInfo(slot, file.Name, file.Size);
SetFileName(slot, file.Name);
SetBrowserFile(slot, file);
_ = ModelChanged.InvokeAsync(Model);
}
catch
{
SetFileError(slot, "Failed to read file");
}
}
private void RemoveFile(FileSlot slot)
{
SetFileInfo(slot, string.Empty, 0);
SetFileError(slot, null);
SetFileName(slot, string.Empty);
_ = ModelChanged.InvokeAsync(Model);
}
private void SetFileError(FileSlot slot, string? error)
{
switch (slot)
{
case FileSlot.Ca: CaFileError = error; break;
case FileSlot.Cert: CertFileError = error; break;
case FileSlot.Key: KeyFileError = error; break;
}
}
private void SetFileInfo(FileSlot slot, string? name, long size)
{
switch (slot)
{
case FileSlot.Ca: CaFileInfo = string.IsNullOrEmpty(name) ? "" : $"{name} {FormatSize(size)}"; break;
case FileSlot.Cert: CertFileInfo = string.IsNullOrEmpty(name) ? "" : $"{name} {FormatSize(size)}"; break;
case FileSlot.Key: KeyFileInfo = string.IsNullOrEmpty(name) ? "" : $"{name} {FormatSize(size)}"; break;
}
}
private void SetFileName(FileSlot slot, string? name)
{
switch (slot)
{
case FileSlot.Ca: Model.VDA5050CA = name; break;
case FileSlot.Cert: Model.VDA5050Cer = name; break;
case FileSlot.Key: Model.VDA5050Key = name; break;
}
}
private void SetBrowserFile(FileSlot slot, IBrowserFile file)
{
switch (slot)
{
case FileSlot.Ca: CaFile = file; break;
case FileSlot.Cert: CertFile = file; break;
case FileSlot.Key: KeyFile = file; break;
}
}
private static string FormatSize(long size)
{
if (size <= 0) return "0 B";
if (size < 1024) return $"{size} B";
double kb = size / 1024.0;
if (kb < 1024) return $"{kb:F1} KB";
double mb = kb / 1024.0;
return $"{mb:F2} MB";
}
protected override void OnParametersSet()
{
if (EditContext is null || !EditContext.Model!.Equals(Model))
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
EditContext = new EditContext(Model);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
CaFileInfo = string.Empty;
CertFileInfo = string.Empty;
KeyFileInfo = string.Empty;
}
}
private void TogglePasswordVisibility()
{
showPassword = !showPassword;
}
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
_ = ModelChanged.InvokeAsync(Model);
}
public void Dispose()
{
if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
}
}

View File

@@ -0,0 +1,93 @@
/* wrapper positions the toggle inside the input */
.password-input-wrapper {
position: relative;
display: block;
}
/* add right padding so text doesn't overlap the icon */
.password-input {
padding-right: 2.25rem; /* adjust as needed for icon size */
}
/* icon button sits absolutely inside the input at the right */
.password-toggle-btn {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
border: none;
background: transparent;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: inherit;
line-height: 1;
}
/* keep the native focus behavior but provide visible ring for keyboard users */
.password-toggle-btn:focus {
outline: none;
}
.password-toggle-btn:focus-visible {
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
border-radius: 0.25rem;
}
/* tune icon size */
.password-toggle-btn .mdi {
font-size: 1.15rem;
pointer-events: none; /* let clicks hit the button, not the icon */
}
.file-input-wrapper {
max-width: 100%;
}
.custom-file-input-wrapper {
max-width: 100%;
}
.custom-file-input-wrapper .form-control {
height: 38px;
padding-right: 50px;
background-color: #f8f9fa;
border: 1px solid #ced4da;
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* === NÚT LABEL GIỐNG BUTTON THẬT === */
.upload-btn {
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
min-width: 50px;
font-size: 23px;
justify-content: center;
border-right: 1px solid silver;
border-radius: 0.25rem;
}
/* HOVER: đổi màu nền + viền */
.upload-btn:hover {
background-color: #e7f3ff !important;
}
/* FOCUS: khi tab đến (accessibility) */
.upload-btn:focus {
outline: 2px solid transparent;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.5) !important;
}
/* ACTIVE: khi nhấn */
.upload-btn:active {
transform: translateY(0);
background-color: #d0e8ff !important;
}

View File

@@ -0,0 +1,136 @@
@inject HttpClient Http
@inject IJSRuntime JS
<div @ref="ViewContainerRef" class="w-100 h-100">
<MudTable Class="h-100 w-100" @ref="Table" Items="@MapsShow" T="MapDto" Dense Hover ReadOnly FixedHeader RowClass="cursor-pointer" Striped Elevation="10"
ServerData="ReloadData" Loading=@IsLoading Height="@($"{TableHeight}px")" HorizontalScrollbar=true>
<ToolBarContent>
<h4>Maps</h4>
</ToolBarContent>
<HeaderContent>
<MudTh>Nr</MudTh>
<MudTh>Name</MudTh>
<MudTh>Width (m)</MudTh>
<MudTh>Height (m)</MudTh>
<MudTh>Resolution (m/px)</MudTh>
<MudTh>OriginX</MudTh>
<MudTh>OriginY</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Nr">
@(Table?.CurrentPage * Table?.RowsPerPage + MapsShow.IndexOf(context) + 1)
</MudTd>
<MudTd DataLabel="Name">
@context.Name
</MudTd>
<MudTd DataLabel="Width">
@context.Width
</MudTd>
<MudTd DataLabel="Height">
@context.Height
</MudTd>
<MudTd DataLabel="Resolution">
@context.Resolution
</MudTd>
<MudTd DataLabel="OriginX">
@context.OriginX
</MudTd>
<MudTd DataLabel="OriginY">
@context.OriginY
</MudTd>
<MudTd>
<MudMenuItem Icon="@Icons.Material.Filled.Edit" IconColor="Color.Info">Active</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error">Delete</MudMenuItem>
</MudTd>
</RowTemplate>
<PagerContent>
<div class="d-flex w-100 flex-row-reverse">
<MudTablePager Style="width: 100%;" PageSizeOptions="new[] { 25, 100, 200 }" />
</div>
</PagerContent>
</MudTable>
</div>
@code {
private string txtSearch = "";
private bool IsLoading = false;
private List<MapDto> Maps = [];
private List<MapDto> MapsShow = [];
private MapDto MapSelected = new();
private MudTable<MapDto>? Table;
private ElementReference ViewContainerRef;
private double TableHeight = 105;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!firstRender) return;
var containerSize = await JS.InvokeAsync<DomRect>("getElementSize", ViewContainerRef);
TableHeight = containerSize.Height - 105;
// await LoadMaps();
StateHasChanged();
}
private async Task LoadMaps()
{
try
{
IsLoading = true;
Maps.Clear();
StateHasChanged();
var maps = await Http.GetFromJsonAsync<IEnumerable<MapDto>>($"api/MapsManager?txtSearch={txtSearch}");
Maps.AddRange(maps ?? []);
Table?.ReloadServerData();
IsLoading = false;
StateHasChanged();
}
catch
{
return;
}
}
private void TextSearchChanged(string text)
{
txtSearch = text;
Table?.ReloadServerData();
}
private bool FilterFunc(MapDto map)
{
if (string.IsNullOrWhiteSpace(txtSearch))
return true;
if (map.Name is not null && map.Name.Contains(txtSearch, StringComparison.OrdinalIgnoreCase))
return true;
if ($"{map.Name}".Contains(txtSearch))
return true;
return false;
}
private Task<TableData<MapDto>> ReloadData(TableState state, CancellationToken _)
{
MapsShow.Clear();
var tasks = new List<MapDto>();
Maps.ForEach(map =>
{
if (FilterFunc(map)) tasks.Add(map);
});
MapsShow = tasks.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList();
return Task.FromResult(new TableData<MapDto>() { TotalItems = tasks.Count, Items = MapsShow });
}
public class DomRect
{
public double Width { get; set; }
public double Height { get; set; }
}
}

View File

@@ -0,0 +1,5 @@
.map-preview {
width: 20%;
height: 100%;
border-left: 1px solid silver;
}

View File

@@ -0,0 +1,466 @@
@inject IJSRuntime JS
@using Excubo.Blazor.Canvas.Contexts
<div class="view">
<div class="toolbar">
<MudTooltip Text="Zoom In" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomIn">
<i class="mdi mdi-magnify-plus-outline icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Zoom Out" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomOut">
<i class="mdi mdi-magnify-minus-outline icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Reset View" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" @onclick="ResetView">
<i class="mdi mdi-fit-to-screen-outline icon-button"></i>
</button>
</MudTooltip>
<MudSpacer />
<MudTooltip Text="Start Localization" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" disabled="@(false)">
<i class="mdi mdi-play icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Stop Localization" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" disabled="@(true)">
<i class="mdi mdi-pause icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Start Mapping" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" disabled="@(false)">
<i class="mdi mdi-plus icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Stop Mapping" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" disabled="@(true)">
<i class="mdi mdi-stop icon-button"></i>
</button>
</MudTooltip>
</div>
<div @ref="ViewContainerRef">
<canvas @ref="CanvasRef"
@onwheel="HandleWheel"
@onmousemove="HandleMouseMove"
@onmouseleave="HandleMouseLeave"
@ontouchstart="HandleTouchStart"
@ontouchmove="HandleTouchMove"
@ontouchend="HandleTouchEnd"></canvas>
</div>
</div>
@code {
private ElementReference CanvasRef;
private ElementReference ViewContainerRef;
private double ZoomScale = 1.0;
private const double MIN_ZOOM = 0.1;
private const double MAX_ZOOM = 5.0;
private const double BASE_PIXELS_PER_METER = 50.0;
private bool IsMouseInCanvas = false;
private double MouseX;
private double MouseY;
private double OriginX = 0;
private double OriginY = 0;
private double WorldMouseX;
private double WorldMouseY;
private double CanvasWidth;
private double CanvasHeight;
private double CanvasTranslateX = 0;
private double CanvasTranslateY = 0;
private const double RulerHeight = 20;
private const double RobotWidth = 0.606;
private const double RobotLength = 1.106;
private bool RobotImageLoaded = false;
private bool MapImageLoaded = false;
private const double ImageX = -10;
private const double ImageY = -5;
private const double ImageResolution = 0.05;
private double MapImageWidth = 0;
private double MapImageHeight = 0;
private const string MAP_CACHE_KEY = "map_image";
private TouchPoint? LastTouchPoint;
private TouchPoint? LastSecondTouchPoint;
private double LastTouchDistance = 0;
private bool IsTouching = false;
protected override async Task OnAfterRenderAsync(bool first_render)
{
await base.OnAfterRenderAsync(first_render);
if (!first_render) return;
var containerSize = await JS.InvokeAsync<DomRect>("getElementSize", ViewContainerRef);
CanvasWidth = containerSize.Width;
CanvasHeight = containerSize.Height;
await JS.InvokeVoidAsync("setCanvasSize", CanvasRef, CanvasWidth, CanvasHeight);
CanvasTranslateX = CanvasWidth / 2;
CanvasTranslateY = CanvasHeight / 2;
await LoadRobotImage();
await LoadMapImage();
await DrawCanvas();
}
private async Task LoadRobotImage()
{
try
{
await JS.InvokeVoidAsync("preloadImage", "images/AMR-250.png");
RobotImageLoaded = true;
}
catch
{
RobotImageLoaded = false;
}
}
private async Task LoadMapImage()
{
try
{
MapImageLoaded = false;
string apiUrl = "api/images/mapping";
await JS.InvokeVoidAsync("preloadImageFromUrl", apiUrl, MAP_CACHE_KEY);
var imageDimensions = await JS.InvokeAsync<DomRect>("getImageDimensions", MAP_CACHE_KEY);
MapImageWidth = imageDimensions.Width * ImageResolution;
MapImageHeight = imageDimensions.Height * ImageResolution;
if (MapImageWidth > 0 && MapImageHeight > 0) MapImageLoaded = true;
}
catch
{
MapImageLoaded = false;
}
}
private async Task ResetView()
{
CanvasTranslateX = CanvasWidth / 2;
CanvasTranslateY = CanvasHeight / 2;
ZoomScale = 1.0;
StateHasChanged();
await DrawCanvas();
}
private async Task ZoomIn()
{
const double zoomFactor = 0.15;
double oldZoom = ZoomScale;
ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor));
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
await ZoomAtCenter(oldZoom);
}
private async Task ZoomOut()
{
const double zoomFactor = 0.15;
double oldZoom = ZoomScale;
ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor));
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
await ZoomAtCenter(oldZoom);
}
private async Task ZoomAtCenter(double oldZoom)
{
double centerX = CanvasWidth / 2;
double centerY = CanvasHeight / 2;
double centerWorldX = (centerX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX;
double centerWorldY = (centerY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY;
double newCenterCanvasX = (centerWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale;
double newCenterCanvasY = (centerWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale;
CanvasTranslateX = centerX - newCenterCanvasX;
CanvasTranslateY = centerY - newCenterCanvasY;
if (IsMouseInCanvas)
{
WorldMouseX = CanvasToWorldX(MouseX);
WorldMouseY = CanvasToWorldY(MouseY);
}
StateHasChanged();
await DrawCanvas();
}
private async Task HandleMouseMove(MouseEventArgs e)
{
MouseX = e.OffsetX;
MouseY = e.OffsetY;
IsMouseInCanvas = true;
WorldMouseX = CanvasToWorldX(MouseX);
WorldMouseY = CanvasToWorldY(MouseY);
StateHasChanged();
if (e.Buttons == 4)
{
CanvasTranslateX += e.MovementX;
CanvasTranslateY -= e.MovementY;
}
await DrawCanvas();
}
private async Task HandleMouseLeave(MouseEventArgs e)
{
IsMouseInCanvas = false;
MouseX = 0;
MouseY = 0;
StateHasChanged();
await DrawCanvas();
}
private async Task HandleWheel(WheelEventArgs e)
{
if (e.Buttons == 4) return;
const double zoomFactor = 0.1;
double oldZoom = ZoomScale;
if (e.DeltaY < 0) ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor));
else ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor));
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
MouseX = e.OffsetX;
MouseY = e.OffsetY;
double zoomPointWorldX = (MouseX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX;
double zoomPointWorldY = (MouseY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY;
double newZoomPointCanvasX = (zoomPointWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale;
double newZoomPointCanvasY = (zoomPointWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale;
CanvasTranslateX = MouseX - newZoomPointCanvasX;
CanvasTranslateY = MouseY - newZoomPointCanvasY;
WorldMouseX = CanvasToWorldX(MouseX);
WorldMouseY = CanvasToWorldY(MouseY);
StateHasChanged();
await DrawCanvas();
}
private async Task HandleTouchMove(TouchEventArgs e)
{
if (IsTouching)
{
if (e.Touches.Length == 1)
{
await HandleSingleTouchMove(e.Touches[0]);
}
else if (e.Touches.Length == 2)
{
await HandlePinchZoom(e.Touches[0], e.Touches[1]);
}
StateHasChanged();
await DrawCanvas();
}
}
private void HandleTouchStart(TouchEventArgs e)
{
IsTouching = true;
if (e.Touches.Length == 1)
{
LastTouchPoint = new TouchPoint
{
X = e.Touches[0].ClientX,
Y = e.Touches[0].ClientY
};
}
else if (e.Touches.Length == 2)
{
LastTouchPoint = new TouchPoint
{
X = e.Touches[0].ClientX,
Y = e.Touches[0].ClientY
};
LastSecondTouchPoint = new TouchPoint
{
X = e.Touches[1].ClientX,
Y = e.Touches[1].ClientY
};
LastTouchDistance = CalculateTouchDistance(LastTouchPoint, LastSecondTouchPoint);
}
}
private void HandleTouchEnd(TouchEventArgs e)
{
IsTouching = false;
LastTouchPoint = null;
LastSecondTouchPoint = null;
LastTouchDistance = 0;
}
private async Task HandleSingleTouchMove(Microsoft.AspNetCore.Components.Web.TouchPoint touch)
{
if (LastTouchPoint == null) return;
var currentPoint = new TouchPoint
{
X = touch.ClientX,
Y = touch.ClientY
};
double deltaX = currentPoint.X - LastTouchPoint.X;
double deltaY = currentPoint.Y - LastTouchPoint.Y;
CanvasTranslateX += deltaX;
CanvasTranslateY += deltaY;
LastTouchPoint = currentPoint;
var canvasRect = await JS.InvokeAsync<DomRect>("getElementBoundingRect", CanvasRef);
MouseX = currentPoint.X - canvasRect.X;
MouseY = currentPoint.Y - canvasRect.Y;
IsMouseInCanvas = true;
WorldMouseX = CanvasToWorldX(MouseX);
WorldMouseY = CanvasToWorldY(MouseY);
}
private async Task HandlePinchZoom(Microsoft.AspNetCore.Components.Web.TouchPoint touch1, Microsoft.AspNetCore.Components.Web.TouchPoint touch2)
{
if (LastTouchPoint == null || LastSecondTouchPoint == null) return;
var currentTouch1 = new TouchPoint { X = touch1.ClientX, Y = touch1.ClientY };
var currentTouch2 = new TouchPoint { X = touch2.ClientX, Y = touch2.ClientY };
double currentDistance = CalculateTouchDistance(currentTouch1, currentTouch2);
if (LastTouchDistance > 0)
{
double distanceRatio = currentDistance / LastTouchDistance;
double oldZoom = ZoomScale;
ZoomScale = Math.Max(MIN_ZOOM, Math.Min(MAX_ZOOM, ZoomScale * distanceRatio));
if (Math.Abs(ZoomScale - oldZoom) > 0.001)
{
double centerX = (currentTouch1.X + currentTouch2.X) / 2;
double centerY = (currentTouch1.Y + currentTouch2.Y) / 2;
var canvasRect = await JS.InvokeAsync<DomRect>("getElementBoundingRect", CanvasRef);
double canvasCenterX = centerX - canvasRect.X;
double canvasCenterY = centerY - canvasRect.Y;
ZoomAtPoint(oldZoom, canvasCenterX, canvasCenterY);
}
}
LastTouchDistance = currentDistance;
LastTouchPoint = currentTouch1;
LastSecondTouchPoint = currentTouch2;
}
private double CalculateTouchDistance(TouchPoint point1, TouchPoint point2)
{
double deltaX = point2.X - point1.X;
double deltaY = point2.Y - point1.Y;
return Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
}
private void ZoomAtPoint(double oldZoom, double pointX, double pointY)
{
double pointWorldX = (pointX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX;
double pointWorldY = (pointY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY;
double newPointCanvasX = (pointWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale;
double newPointCanvasY = (pointWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale;
CanvasTranslateX = pointX - newPointCanvasX;
CanvasTranslateY = pointY - newPointCanvasY;
}
private LaserScanData GenerateLaserScanData()
{
// Robot position (in world coordinates)
double robotX = 2; // Robot at origin for demo
double robotY = 2;
double robotOrientation = 0; // Robot facing right (0 degrees)
Random random = new Random(42); // Fixed seed for consistent pattern
// Laser scanner parameters
const double maxRange = 8.0; // meters
const double minRange = 0.5; // meters (fix: was 7.0, should be minimum)
const int numPoints = 270; // Number of laser points
const double startAngle = -Math.PI / 2 - Math.PI / 4;
const double endAngle = Math.PI / 2 + Math.PI / 4;
double angleStep = (endAngle - startAngle) / (numPoints - 1);
var scanData = new LaserScanData
{
RobotX = robotX,
RobotY = robotY,
RobotOrientation = robotOrientation,
};
// Generate laser points
for (int i = 0; i < numPoints; i++)
{
double angle = startAngle + i * angleStep;
// Random range with some clustering around obstacles
double range;
if (random.NextDouble() < 0.3) // 30% chance of obstacles
{
range = random.NextDouble() * 3.0 + 1.0; // 1-4 meters (obstacles)
}
else if (random.NextDouble() < 0.1) // 10% chance of very close objects
{
range = random.NextDouble() * 0.8 + 0.2; // 0.2-1.0 meters
}
else
{
range = random.NextDouble() * maxRange * 0.7 + maxRange * 0.3; // Far points
}
// Add some noise to make it realistic
range += (random.NextDouble() - 0.5) * 0.1;
range = Math.Max(minRange, Math.Min(maxRange, range));
// Calculate point position relative to robot
double pointX = robotX + Math.Cos(angle + robotOrientation) * range;
double pointY = robotY + Math.Sin(angle + robotOrientation) * range;
scanData.Points.Add(new LaserScanPoint
{
X = pointX,
Y = pointY
});
}
return scanData;
}
}

View File

@@ -0,0 +1,576 @@
using Excubo.Blazor.Canvas;
using Excubo.Blazor.Canvas.Contexts;
using Microsoft.JSInterop;
namespace RobotApp.Client.Pages.Components.Mapping;
public partial class MapView
{
public class TouchPoint
{
public double X { get; set; }
public double Y { get; set; }
}
public class LaserScanPoint
{
public double X { get; set; }
public double Y { get; set; }
}
public class LaserScanData
{
public double RobotX { get; set; }
public double RobotY { get; set; }
public double RobotOrientation { get; set; }
public List<LaserScanPoint> Points { get; set; } = [];
}
public class DomRect
{
public double Width { get; set; }
public double Height { get; set; }
public double X { get; set; }
public double Y { get; set; }
public double Left => X;
public double Top => Y;
}
private async Task DrawCanvas()
{
await using var ctx = await JS.GetContext2DAsync(CanvasRef);
await ctx.ClearRectAsync(0, 0, CanvasWidth, CanvasHeight);
await ctx.SaveAsync();
await ctx.TranslateAsync(CanvasTranslateX, CanvasTranslateY);
await ctx.ScaleAsync(ZoomScale, ZoomScale);
await DrawMapImage(ctx);
await DrawGrid(ctx);
await DrawAxes(ctx);
await DrawLaserScannerPoints(ctx);
await ctx.RestoreAsync();
if (IsMouseInCanvas)
{
await DrawMouseIndicator(ctx);
}
await DrawRulers(ctx);
}
private async Task DrawMouseIndicator(Context2D ctx)
{
await ctx.SaveAsync();
await ctx.StrokeStyleAsync("rgba(255, 50, 50, 0.8)");
await ctx.LineWidthAsync(1);
await ctx.SetLineDashAsync([3, 3]);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(MouseX, RulerHeight);
await ctx.LineToAsync(MouseX, CanvasHeight);
await ctx.StrokeAsync();
await ctx.BeginPathAsync();
await ctx.MoveToAsync(RulerHeight, MouseY);
await ctx.LineToAsync(CanvasWidth, MouseY);
await ctx.StrokeAsync();
await ctx.SetLineDashAsync([]);
const double labelPadding = 7;
const double labelMargin = 8;
string coordinateText = $"({WorldMouseX:F2}m, {WorldMouseY:F2}m)";
await ctx.FontAsync("bold 12px Arial");
var textMetrics = await ctx.MeasureTextAsync(coordinateText);
double textWidth = textMetrics.Width;
double textHeight = 16;
double labelX = MouseX + labelMargin;
double labelY = MouseY - textHeight - labelPadding * 2 - labelMargin;
if (labelX + textWidth + labelPadding * 2 > CanvasWidth)
{
labelX = MouseX - textWidth - labelPadding * 2 - labelMargin;
}
if (labelY - textHeight - labelPadding * 2 < RulerHeight)
{
labelY = MouseY + labelMargin;
}
await ctx.FillStyleAsync("rgba(0, 0, 0, 0.8)");
await ctx.FillRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2);
await ctx.StrokeStyleAsync("rgba(255,255,255,0.6)");
await ctx.LineWidthAsync(1);
await ctx.StrokeRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2);
await ctx.FillStyleAsync("rgba(255, 50, 50, 0.9)");
await ctx.BeginPathAsync();
await ctx.ArcAsync(MouseX, MouseY, 3, 0, Math.PI * 2);
await ctx.FillAsync(FillRule.NonZero);
await ctx.StrokeStyleAsync("rgba(255, 255, 255, 0.8)");
await ctx.LineWidthAsync(2);
await ctx.BeginPathAsync();
await ctx.ArcAsync(MouseX, MouseY, 6, 0, Math.PI * 2);
await ctx.StrokeAsync();
await ctx.SaveAsync();
await ctx.TranslateAsync(labelX + labelPadding + textWidth / 2, labelY + textHeight / 2);
await ctx.ScaleAsync(1, -1);
await ctx.FillStyleAsync("white");
await ctx.FontAsync("bold 12px Arial");
await ctx.TextAlignAsync(TextAlign.Center);
await ctx.TextBaseLineAsync(TextBaseLine.Bottom);
await ctx.FillTextAsync(coordinateText, 0, 0);
await ctx.RestoreAsync();
}
private async Task DrawRulers(Context2D ctx)
{
double visibleWorldLeft = CanvasToWorldX(0);
double visibleWorldRight = CanvasToWorldX(CanvasWidth);
double visibleWorldTop = CanvasToWorldY(0);
double visibleWorldBottom = CanvasToWorldY(CanvasHeight);
double scaleInterval = GetRulerScaleInterval();
await DrawXRuler(ctx, RulerHeight, visibleWorldLeft, visibleWorldRight, scaleInterval);
await DrawYRuler(ctx, RulerHeight, visibleWorldTop, visibleWorldBottom, scaleInterval);
}
private double GetRulerScaleInterval()
{
double pixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale;
if (pixelsPerMeter >= 400) return 0.1;
else if (pixelsPerMeter >= 200) return 0.2;
else if (pixelsPerMeter >= 100) return 0.5;
else if (pixelsPerMeter >= 50) return 1.0;
else if (pixelsPerMeter >= 25) return 2.0;
else if (pixelsPerMeter >= 12) return 5.0;
else if (pixelsPerMeter >= 6) return 10.0;
else return 20.0;
}
private async Task DrawXRuler(Context2D ctx, double rulerHeight, double visibleWorldLeft, double visibleWorldRight, double scaleInterval)
{
await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)");
await ctx.FillRectAsync(0, 0, CanvasWidth, rulerHeight);
await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)");
await ctx.LineWidthAsync(1);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(0, rulerHeight);
await ctx.LineToAsync(CanvasWidth, rulerHeight);
await ctx.StrokeAsync();
double startWorld = Math.Floor(visibleWorldLeft / scaleInterval) * scaleInterval;
double endWorld = Math.Ceiling(visibleWorldRight / scaleInterval) * scaleInterval;
startWorld -= scaleInterval;
endWorld += scaleInterval;
for (double worldX = startWorld; worldX <= endWorld; worldX += scaleInterval)
{
double canvasX = WorldToCanvasX(worldX);
if (canvasX < -50 || canvasX > CanvasWidth + 50) continue;
bool isMajorTick = IsNearMultiple(worldX, scaleInterval * 2) || Math.Abs(worldX) < 0.001;
double tickHeight = isMajorTick ? rulerHeight * 0.4 : rulerHeight * 0.2;
await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)");
await ctx.LineWidthAsync(1);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(canvasX, rulerHeight);
await ctx.LineToAsync(canvasX, rulerHeight - tickHeight);
await ctx.StrokeAsync();
if (isMajorTick && canvasX >= -20 && canvasX <= CanvasWidth + 20)
{
await ctx.SaveAsync();
await ctx.TranslateAsync(canvasX, rulerHeight - tickHeight - 8);
await ctx.ScaleAsync(1, -1);
await ctx.FillStyleAsync("blue");
await ctx.FontAsync("bold 10px Arial");
await ctx.TextAlignAsync(TextAlign.Center);
string labelText = FormatRulerLabel(worldX, scaleInterval);
await ctx.FillTextAsync(labelText, 0, 0);
await ctx.RestoreAsync();
}
}
}
private async Task DrawYRuler(Context2D ctx, double rulerWidth, double visibleWorldTop, double visibleWorldBottom, double scaleInterval)
{
await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)");
await ctx.FillRectAsync(0, 0, rulerWidth, CanvasHeight);
await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)");
await ctx.LineWidthAsync(1);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(rulerWidth, 0);
await ctx.LineToAsync(rulerWidth, CanvasHeight);
await ctx.StrokeAsync();
double startWorld = Math.Floor(visibleWorldTop / scaleInterval) * scaleInterval;
double endWorld = Math.Ceiling(visibleWorldBottom / scaleInterval) * scaleInterval;
startWorld -= scaleInterval;
endWorld += scaleInterval;
for (double worldY = startWorld; worldY <= endWorld; worldY += scaleInterval)
{
double canvasY = WorldToCanvasY(worldY);
if (canvasY < -50 || canvasY > CanvasHeight + 50) continue;
bool isMajorTick = IsNearMultiple(worldY, scaleInterval * 2) || Math.Abs(worldY) < 0.001;
double tickWidth = isMajorTick ? rulerWidth * 0.4 : rulerWidth * 0.2;
await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)");
await ctx.LineWidthAsync(1);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(rulerWidth, canvasY);
await ctx.LineToAsync(rulerWidth - tickWidth, canvasY);
await ctx.StrokeAsync();
if (isMajorTick && canvasY >= -20 && canvasY <= CanvasHeight + 20)
{
await ctx.SaveAsync();
await ctx.TranslateAsync(rulerWidth - tickWidth - 2, canvasY);
await ctx.ScaleAsync(1, -1);
await ctx.RotateAsync(-Math.PI / 2);
await ctx.FillStyleAsync("blue");
await ctx.FontAsync("bold 10px Arial");
await ctx.TextAlignAsync(TextAlign.Center);
string labelText = FormatRulerLabel(worldY, scaleInterval);
await ctx.FillTextAsync(labelText, 0, 0);
await ctx.RestoreAsync();
}
}
}
private static bool IsNearMultiple(double value, double multiple)
{
if (multiple == 0) return false;
double remainder = Math.Abs(value % multiple);
double epsilon = multiple * 0.001;
return remainder < epsilon || remainder > multiple - epsilon;
}
private static string FormatRulerLabel(double worldValue, double scaleInterval)
{
return scaleInterval < 1.0 ? $"{worldValue:F1}m" : $"{worldValue:F0}m";
}
private async Task DrawAxes(Context2D ctx)
{
double originCanvasX = OriginX * BASE_PIXELS_PER_METER;
double originCanvasY = OriginY * BASE_PIXELS_PER_METER;
await ctx.FillStyleAsync("red");
await ctx.BeginPathAsync();
await ctx.ArcAsync(originCanvasX, originCanvasY, 8 / ZoomScale, 0, Math.PI * 2);
await ctx.FillAsync(FillRule.NonZero);
double gridSpacingMeters = GetGridSpacingMeters();
double arrowLength = gridSpacingMeters * BASE_PIXELS_PER_METER;
double arrowHeadSize = 16 / ZoomScale;
await ctx.FillStyleAsync("blue");
await ctx.StrokeStyleAsync("blue");
await ctx.LineWidthAsync(4 / ZoomScale);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(originCanvasX, originCanvasY);
await ctx.LineToAsync(originCanvasX + arrowLength - arrowHeadSize, originCanvasY);
await ctx.StrokeAsync();
await ctx.BeginPathAsync();
double xArrowTipX = originCanvasX + arrowLength;
double xArrowTipY = originCanvasY;
await ctx.MoveToAsync(xArrowTipX, xArrowTipY);
await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY - arrowHeadSize / 2);
await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY + arrowHeadSize / 2);
await ctx.ClosePathAsync();
await ctx.FillAsync(FillRule.NonZero);
await ctx.FillStyleAsync("red");
await ctx.StrokeStyleAsync("red");
await ctx.LineWidthAsync(4 / ZoomScale);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(originCanvasX, originCanvasY);
await ctx.LineToAsync(originCanvasX, originCanvasY + arrowLength - arrowHeadSize);
await ctx.StrokeAsync();
await ctx.BeginPathAsync();
double yArrowTipX = originCanvasX;
double yArrowTipY = originCanvasY + arrowLength;
await ctx.MoveToAsync(yArrowTipX, yArrowTipY);
await ctx.LineToAsync(yArrowTipX - arrowHeadSize / 2, yArrowTipY - arrowHeadSize);
await ctx.LineToAsync(yArrowTipX + arrowHeadSize / 2, yArrowTipY - arrowHeadSize);
await ctx.ClosePathAsync();
await ctx.FillAsync(FillRule.NonZero);
}
private async Task DrawGrid(Context2D ctx)
{
await ctx.StrokeStyleAsync("rgba(200, 200, 200, 0.4)");
await ctx.LineWidthAsync(1 / ZoomScale);
await ctx.SetLineDashAsync([5 / ZoomScale, 5 / ZoomScale]);
double gridSpacingMeters = GetGridSpacingMeters();
double gridSpacingPixels = gridSpacingMeters * BASE_PIXELS_PER_METER;
double visibleLeft = -CanvasTranslateX / ZoomScale;
double visibleRight = (CanvasWidth - CanvasTranslateX) / ZoomScale;
double visibleTop = -CanvasTranslateY / ZoomScale;
double visibleBottom = (CanvasHeight - CanvasTranslateY) / ZoomScale;
double startX = Math.Floor(visibleLeft / gridSpacingPixels) * gridSpacingPixels;
double startY = Math.Floor(visibleTop / gridSpacingPixels) * gridSpacingPixels;
for (double x = startX; x <= visibleRight; x += gridSpacingPixels)
{
await ctx.BeginPathAsync();
await ctx.MoveToAsync(x, visibleTop);
await ctx.LineToAsync(x, visibleBottom);
await ctx.StrokeAsync();
}
for (double y = startY; y <= visibleBottom; y += gridSpacingPixels)
{
await ctx.BeginPathAsync();
await ctx.MoveToAsync(visibleLeft, y);
await ctx.LineToAsync(visibleRight, y);
await ctx.StrokeAsync();
}
await ctx.SetLineDashAsync([]);
}
private double GetGridSpacingMeters()
{
double PixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale;
if (PixelsPerMeter >= 300) return 0.2;
else if (PixelsPerMeter >= 150) return 0.5;
else if (PixelsPerMeter >= 75) return 1.0;
else if (PixelsPerMeter >= 40) return 2.0;
else if (PixelsPerMeter >= 20) return 5.0;
else if (PixelsPerMeter >= 10) return 10.0;
else return 20.0;
}
private async Task DrawMapImage(Context2D ctx)
{
if (!MapImageLoaded)
{
return;
}
await ctx.SaveAsync();
try
{
double imageWidthCanvas = MapImageWidth * BASE_PIXELS_PER_METER;
double imageHeightCanvas = MapImageHeight * BASE_PIXELS_PER_METER;
double mapCanvasX = ImageX * BASE_PIXELS_PER_METER;
double mapCanvasY = (ImageY + MapImageHeight) * BASE_PIXELS_PER_METER;
bool success = await JS.InvokeAsync<bool>("drawCachedImageOnCanvas",
CanvasRef,
MAP_CACHE_KEY,
mapCanvasX,
mapCanvasY - imageHeightCanvas,
imageWidthCanvas,
imageHeightCanvas);
}
catch
{
}
await ctx.RestoreAsync();
}
private async Task DrawLaserScannerPoints(Context2D ctx)
{
var scanData = GenerateLaserScanData();
double robotCanvasX = scanData.RobotX * BASE_PIXELS_PER_METER;
double robotCanvasY = scanData.RobotY * BASE_PIXELS_PER_METER;
await ctx.SaveAsync();
if (scanData.Points.Count > 0)
{
await ctx.BeginPathAsync();
for (int i = 0; i < scanData.Points.Count; i++)
{
var point = scanData.Points[i];
double pointCanvasX = point.X * BASE_PIXELS_PER_METER;
double pointCanvasY = point.Y * BASE_PIXELS_PER_METER;
if (i == 0)
{
await ctx.MoveToAsync(pointCanvasX, pointCanvasY);
}
else
{
await ctx.LineToAsync(pointCanvasX, pointCanvasY);
}
}
await ctx.StrokeStyleAsync("rgba(255, 100, 100, 0.8)");
await ctx.LineWidthAsync(2 / ZoomScale);
await ctx.StrokeAsync();
await ctx.LineToAsync(robotCanvasX, robotCanvasY);
await ctx.ClosePathAsync();
await ctx.FillStyleAsync("rgba(255, 100, 100, 0.1)");
await ctx.FillAsync(FillRule.NonZero);
}
await DrawRobotImage(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation);
await DrawRobotOrientationArrows(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation);
await ctx.RestoreAsync();
}
private async Task DrawRobotImage(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation)
{
if (!RobotImageLoaded)
{
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
await ctx.BeginPathAsync();
await ctx.ArcAsync(robotCanvasX, robotCanvasY, 8 / ZoomScale, 0, Math.PI * 2);
await ctx.FillAsync(FillRule.NonZero);
return;
}
await ctx.SaveAsync();
double robotWidthPixels = RobotWidth * BASE_PIXELS_PER_METER;
double robotLengthPixels = RobotLength * BASE_PIXELS_PER_METER;
double scaledWidth = ZoomScale < 1 ? robotWidthPixels / ZoomScale : robotWidthPixels;
double scaledLength = ZoomScale < 1 ? robotLengthPixels / ZoomScale : robotLengthPixels;
await ctx.TranslateAsync(robotCanvasX, robotCanvasY);
await ctx.RotateAsync(robotOrientation);
try
{
bool success = await JS.InvokeAsync<bool>("drawImageOnCanvas",
CanvasRef,
"images/AMR-250.png",
-scaledLength / 2,
-scaledWidth / 2,
scaledLength,
scaledWidth);
if (!success)
{
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth);
}
}
catch
{
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth);
}
await ctx.RestoreAsync();
}
private async Task DrawRobotOrientationArrows(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation)
{
double arrowLength = 30 / ZoomScale;
double arrowHeadSize = 10 / ZoomScale;
await ctx.StrokeStyleAsync("rgba(0, 100, 255, 1.0)");
await ctx.FillStyleAsync("rgba(0, 100, 255, 1.0)");
await ctx.LineWidthAsync(3 / ZoomScale);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(robotCanvasX, robotCanvasY);
double xAxisEndX = robotCanvasX + Math.Cos(robotOrientation) * (arrowLength - arrowHeadSize + 1);
double xAxisEndY = robotCanvasY + Math.Sin(robotOrientation) * (arrowLength - arrowHeadSize + 1);
await ctx.LineToAsync(xAxisEndX, xAxisEndY);
await ctx.StrokeAsync();
await ctx.BeginPathAsync();
double xArrowTipX = robotCanvasX + Math.Cos(robotOrientation) * arrowLength;
double xArrowTipY = robotCanvasY + Math.Sin(robotOrientation) * arrowLength;
await ctx.MoveToAsync(xArrowTipX, xArrowTipY);
double xArrowAngle = robotOrientation + Math.PI;
await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle + Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle + Math.PI / 6) * arrowHeadSize);
await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle - Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle - Math.PI / 6) * arrowHeadSize);
await ctx.ClosePathAsync();
await ctx.FillAsync(FillRule.NonZero);
await ctx.StrokeStyleAsync("rgba(255, 50, 50, 1.0)");
await ctx.FillStyleAsync("rgba(255, 50, 50, 1.0)");
await ctx.LineWidthAsync(3 / ZoomScale);
double yAxisAngle = robotOrientation + Math.PI / 2;
await ctx.BeginPathAsync();
await ctx.MoveToAsync(robotCanvasX, robotCanvasY);
double yAxisEndX = robotCanvasX + Math.Cos(yAxisAngle) * (arrowLength - arrowHeadSize + 1);
double yAxisEndY = robotCanvasY + Math.Sin(yAxisAngle) * (arrowLength - arrowHeadSize + 1);
await ctx.LineToAsync(yAxisEndX, yAxisEndY);
await ctx.StrokeAsync();
await ctx.BeginPathAsync();
double yArrowTipX = robotCanvasX + Math.Cos(yAxisAngle) * arrowLength;
double yArrowTipY = robotCanvasY + Math.Sin(yAxisAngle) * arrowLength;
await ctx.MoveToAsync(yArrowTipX, yArrowTipY);
double yArrowAngle = yAxisAngle + Math.PI;
await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle + Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle + Math.PI / 6) * arrowHeadSize);
await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle - Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle - Math.PI / 6) * arrowHeadSize);
await ctx.ClosePathAsync();
await ctx.FillAsync(FillRule.NonZero);
}
private double CanvasToWorldX(double canvasX)
{
return (canvasX - CanvasTranslateX) / ZoomScale / BASE_PIXELS_PER_METER - OriginX;
}
private double CanvasToWorldY(double canvasY)
{
return (canvasY - CanvasTranslateY) / ZoomScale / BASE_PIXELS_PER_METER - OriginY;
}
private double WorldToCanvasX(double worldX)
{
return (worldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale + CanvasTranslateX;
}
private double WorldToCanvasY(double worldY)
{
return (worldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale + CanvasTranslateY;
}
}

View File

@@ -0,0 +1,70 @@
.view {
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
background-color: #808080;
border-radius: var(--mud-default-borderradius);
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
box-shadow: var(--mud-elevation-10);
}
.view .toolbar {
width: 100%;
height: 3rem;
flex: 0 0 auto;
display: flex;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid var(--mud-palette-divider);
background-color: var(--mud-palette-background);
}
.view .toolbar .action-button {
padding: 0rem 0.4rem;
margin-right: 0.5rem;
border-radius: var(--mud-default-borderradius);
transition: all 0.3s ease;
background-color: var(--bs-gray-200);
}
.view .toolbar .action-button:hover {
background-color: var(--mud-palette-action-hover);
transform: translateY(-1px);
}
.view .toolbar .action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.view .toolbar .action-button:disabled:hover {
transform: none;
background-color: transparent;
}
.view .toolbar .icon-button {
font-size: 1.2rem;
color: var(--mud-palette-primary);
}
.view > div {
display: flex;
position: relative;
width: 100%;
flex-grow: 1;
overflow: hidden;
}
.view > div > canvas {
transition: cursor 0.2s ease;
display: block;
transform: scale(1, -1);
}
.view > div > canvas:hover {
cursor: crosshair;
}

View File

@@ -0,0 +1,60 @@
<div class="view">
<h4 class="info-title">Informations</h4>
<div class="info-grid">
<div class="info-col">
<dl class="info-list">
<dt class="info-term">X (m)</dt>
<dd class="info-desc">@Localization.X.ToString("F3")</dd>
<dt class="info-term">Y (m)</dt>
<dd class="info-desc">@Localization.Y.ToString("F3")</dd>
<dt class="info-term">Theta (rad)</dt>
<dd class="info-desc">@Localization.Theta.ToString("F4")</dd>
<dt class="info-term">Theta (deg)</dt>
<dd class="info-desc">@($"{Localization.Theta * 180.0 / Math.PI:F2}°")</dd>
<dt class="info-term">Ready</dt>
<dd class="info-desc">@((Localization.IsReady) ? "Yes" : "No")</dd>
</dl>
</div>
<div class="info-col">
<dl class="info-list">
<dt class="info-term">SlamState</dt>
<dd class="info-desc">@Localization.SlamState</dd>
<dt class="info-term">SlamDetail</dt>
<dd class="info-desc text-truncate" title="@Localization.SlamStateDetail">@Localization.SlamStateDetail</dd>
<dt class="info-term">Active Map</dt>
<dd class="info-desc text-truncate" title="@Localization.CurrentActiveMap">@Localization.CurrentActiveMap</dd>
<dt class="info-term">Reliability</dt>
<dd class="info-desc">@($"{Localization.Reliability:F1}%")</dd>
<dt class="info-term">MatchingScore</dt>
<dd class="info-desc">@($"{Localization.MatchingScore:F1}%")</dd>
</dl>
</div>
</div>
</div>
@code {
private class LocalizationDto
{
public bool IsReady { get; set; }
public double X { get; set; }
public double Y { get; set; }
public double Theta { get; set; }
public string SlamState { get; set; } = "Localization";
public string SlamStateDetail { get; set; } = "/r/n";
public string CurrentActiveMap { get; set; } = "Localization";
public double Reliability { get; set; }
public double MatchingScore { get; set; }
}
private LocalizationDto Localization = new();
}

View File

@@ -0,0 +1,121 @@
.view {
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
background-color: var(--mud-palette-surface, #ffffff);
border-radius: var(--mud-default-borderradius, 8px);
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
box-shadow: var(--mud-elevation-10, 0 4px 12px rgba(0,0,0,0.08));
padding: 12px;
box-sizing: border-box;
border: 1px solid rgba(0,0,0,0.04);
}
.view::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
border-top-left-radius: inherit;
border-bottom-left-radius: inherit;
background: linear-gradient(180deg, var(--mud-palette-primary, #1976d2), rgba(25,118,210,0.7));
}
.info-title {
margin: 0 0 10px 12px;
font-weight: 600;
font-size: 1rem;
color: var(--mud-palette-primary, #1976d2);
}
.info-grid {
display: flex;
gap: 14px;
align-items: flex-start;
flex: 1 1 auto;
overflow: hidden;
padding: 1.5em;
}
.info-col {
flex: 1 1 50%;
min-width: 0;
overflow: hidden;
}
.info-list {
margin: 0;
padding: 0;
}
.info-list dt,
.info-list dd {
display: flex;
align-items: center;
padding: 6px 0;
font-size: 0.95rem;
line-height: 1.2;
}
.info-term {
width: 42%;
text-align: right;
padding-right: 12px;
color: var(--mud-palette-text-secondary, rgba(0,0,0,0.6));
font-weight: 600;
box-sizing: border-box;
white-space: nowrap;
}
.info-desc {
width: 58%;
text-align: left;
font-weight: 700;
color: var(--mud-palette-text-primary, rgba(0,0,0,0.85));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.text-truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-list dt + dd {
border-bottom: 1px dashed rgba(0,0,0,0.03);
}
.ready-yes {
color: var(--mud-palette-success, #2e7d32);
}
.ready-no {
color: var(--mud-palette-error, #d32f2f);
}
.percent {
color: var(--mud-palette-primary, #1976d2);
}
@media (max-width: 640px) {
.info-grid {
flex-direction: column;
gap: 8px;
}
.info-term {
width: 45%;
}
.info-desc {
width: 55%;
}
}

View File

@@ -0,0 +1,513 @@
@inject IJSRuntime JS
<div class="robot-monitor-container">
<div class="toolbar">
<MudTooltip Text="Zoom In" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomIn">
<i class="mdi mdi-magnify-plus-outline icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Zoom Out" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomOut">
<i class="mdi mdi-magnify-minus-outline icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Reset View" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" @onclick="ResetView">
<i class="mdi mdi-fit-to-screen-outline icon-button"></i>
</button>
</MudTooltip>
<div class="auto-follow-control">
<MudTooltip Text="Auto Follow Robot" Placement="Placement.Bottom" Color="Color.Info">
<MudSwitch T="bool" Value="AutoFollowRobot" ValueChanged="OnAutoFollowRobotChanged" Color="Color.Info" Size="Size.Small">
<span style="color: white; font-size: 14px; margin-left: 8px;">Follow Robot</span>
</MudSwitch>
</MudTooltip>
</div>
<MudSpacer />
@if (MonitorData?.RobotPosition != null)
{
<div class="robot-position-info">
<i class="mdi mdi-map-marker"></i>
<span>Robot: X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))°</span>
</div>
}
@if (MouseWorldX.HasValue && MouseWorldY.HasValue)
{
<div class="mouse-position-info">
<i class="mdi mdi-cursor-pointer"></i>
<span>Mouse: X: @MouseWorldX.Value.ToString("F2")m | Y: @MouseWorldY.Value.ToString("F2")m</span>
</div>
}
@* <MudChip T="string" Color="@(IsConnected? Color.Success: Color.Error)" Size="Size.Small">
@(IsConnected ? "Connected" : "Disconnected")
</MudChip> *@
</div>
<div @ref="SvgContainerRef" class="svg-container">
<svg @ref="SvgRef"
@onwheel="HandleWheel"
@onmousedown="HandleMouseDown"
@onmousemove="HandleMouseMove"
@onmouseup="HandleMouseUp"
@onmouseleave="HandleMouseLeave">
@* Arrow markers for origin *@
<defs>
<marker id="arrowhead-x" markerWidth="10" markerHeight="10"
refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#FF0000" />
</marker>
<marker id="arrowhead-y" markerWidth="10" markerHeight="10"
refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#00FF00" />
</marker>
</defs>
<g transform="@GetTransform()">
@* Background Map Image *@
@if (MapImageLoaded && MapImageWidth > 0 && MapImageHeight > 0)
{
@* Image origin is at bottom-left corner (MapImageOriginX, MapImageOriginY) in world coordinates
In SVG (after Y flip), image top-left corner is at (MapImageOriginX, -MapImageOriginY - MapImageHeight)
So we render image at: x = MapImageOriginX, y = -MapImageOriginY - MapImageHeight *@
<image href="@MapImageUrl"
x="@WorldToSvgX(MapImageOriginX)"
y="@WorldToSvgY(MapImageOriginY + MapImageHeight)"
width="@MapImageWidth"
height="@MapImageHeight"
preserveAspectRatio="none"
opacity="0.8"
style="pointer-events: none; image-rendering: pixelated;"
id="map-background-image" />
}
@* Origin Marker (2 arrows: X+ and Y+) at (MapImageOriginX, MapImageOriginY) *@
<g transform="@GetOriginMarkerTransform()">
@* X+ Arrow (pointing right) *@
<line x1="0" y1="0" x2="@GetOriginMarkerSize()" y2="0"
stroke="#FF0000" stroke-width="@GetOriginMarkerStrokeWidth()"
marker-end="url(#arrowhead-x)" />
@* Y+ Arrow (pointing up in world, down in SVG) *@
<line x1="0" y1="0" x2="0" y2="@(-GetOriginMarkerSize())"
stroke="#00FF00" stroke-width="@GetOriginMarkerStrokeWidth()"
marker-end="url(#arrowhead-y)" />
@* Origin point *@
<circle cx="0" cy="0" r="@(GetOriginMarkerSize() * 0.12)"
fill="#FFFF00" stroke="#000" stroke-width="@(GetOriginMarkerStrokeWidth() * 0.5)" />
</g>
@if (MonitorData?.HasOrder == true)
{
<path d="@PathView"
fill="none"
stroke="#42A5F5"
stroke-width="0.08"
visibility="@PathIsNot" />
@foreach (var node in MonitorData.NodeStates)
{
<circle cx="@WorldToSvgX(node.NodePosition.X)"
cy="@WorldToSvgY(node.NodePosition.Y)"
r="@GetNodeRadius()"
fill="#66BB6A"
stroke="#555"
stroke-width="@GetNodeStrokeWidth()" />
}
}
@* Render Robot *@
@if (MonitorData?.RobotPosition != null)
{
<g transform="@GetRobotTransform()">
@{
var (robotX, robotY, robotWidth, robotHeight) = GetRobotSize();
}
<image href="images/AMR-250.png"
x="@robotX"
y="@robotY"
width="@robotWidth"
height="@robotHeight"
preserveAspectRatio="xMidYMid meet" />
</g>
}
</g>
</svg>
</div>
</div>
@code {
[Parameter] public RobotMonitorDto? MonitorData { get; set; }
[Parameter] public bool IsConnected { get; set; }
public class ElementSize
{
public double Width { get; set; }
public double Height { get; set; }
}
public class ElementBoundingRect
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
}
private ElementReference SvgRef;
private ElementReference SvgContainerRef;
private double ZoomScale = 1.5; // Zoom vào robot hơn khi mở
private const double MIN_ZOOM = 0.1;
private const double MAX_ZOOM = 5.0;
private const double BASE_PIXELS_PER_METER = 50.0;
private double TranslateX = 0;
private double TranslateY = 0;
private bool IsPanning = false;
private double PanStartX = 0;
private double PanStartY = 0;
private double SvgWidth = 800;
private double SvgHeight = 600;
private string PathView = "";
private string PathIsNot = "hidden";
// Mouse world coordinates
private double? MouseWorldX = null;
private double? MouseWorldY = null;
// Auto-follow robot
private bool AutoFollowRobot = false;
private void OnAutoFollowRobotChanged(bool value)
{
AutoFollowRobot = value;
if (AutoFollowRobot && MonitorData?.RobotPosition != null)
{
UpdateViewToFollowRobot();
StateHasChanged();
}
}
// Map image properties
private const double MapImageOriginX = -20.0; // OriginX in world coordinates (meters)
private const double MapImageOriginY = -20.0; // OriginY in world coordinates (meters)
private const double MapImageResolution = 0.1; // Resolution: meters per pixel
private const string MapImageUrl = "images/gara20250309.png";
private bool MapImageLoaded = false;
private double MapImageWidth = 0; // Width in world coordinates (meters)
private double MapImageHeight = 0; // Height in world coordinates (meters)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var containerSize = await JS.InvokeAsync<ElementSize>("robotMonitor.getElementSize", SvgContainerRef);
SvgWidth = containerSize.Width;
SvgHeight = containerSize.Height;
// Load map image and get dimensions
await LoadMapImage();
// Center view on robot if available with initial zoom
if (MonitorData?.RobotPosition != null)
{
// Zoom vào robot hơn một chút
ZoomScale = 2.5;
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
}
else
{
TranslateX = SvgWidth / 2;
TranslateY = SvgHeight / 2;
}
StateHasChanged();
}
}
private async Task LoadMapImage()
{
try
{
var imageDimensions = await JS.InvokeAsync<ElementSize>("robotMonitor.loadImageAndGetDimensions", MapImageUrl);
// Convert pixel dimensions to world coordinates (meters)
MapImageWidth = imageDimensions.Width * MapImageResolution;
MapImageHeight = imageDimensions.Height * MapImageResolution;
if (MapImageWidth > 0 && MapImageHeight > 0)
{
MapImageLoaded = true;
await InvokeAsync(StateHasChanged); // Force re-render after image is loaded
}
}
catch (Exception ex)
{
MapImageLoaded = false;
Console.WriteLine($"Failed to load map image: {ex.Message}");
}
}
private string GetTransform()
{
// Transform applies: first translate (in screen pixels), then scale (pixels per meter)
// World coordinates are in meters
// After transform: screenX = TranslateX + worldX * (ZoomScale * BASE_PIXELS_PER_METER)
// screenY = TranslateY + worldY * (ZoomScale * BASE_PIXELS_PER_METER)
// But we need to flip Y: screenY = TranslateY - worldY * (ZoomScale * BASE_PIXELS_PER_METER)
// This is handled by WorldToSvgY which flips Y before applying transform
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
}
private string GetRobotTransform()
{
if (MonitorData?.RobotPosition == null) return "";
var x = WorldToSvgX(MonitorData.RobotPosition.X);
var y = WorldToSvgY(MonitorData.RobotPosition.Y);
// Theta là radian, convert sang độ
// SVG rotate quay theo chiều kim đồng hồ, cần đảo dấu
var angleDegrees = -MonitorData.RobotPosition.Theta * 180 / Math.PI;
return $"translate({x}, {y}) rotate({angleDegrees})";
}
private (double x, double y, double width, double height) GetRobotSize()
{
// Kích thước robot trong world coordinates (mét)
const double RobotWidthMeters = 0.606;
const double RobotLengthMeters = 1.106;
// Điều chỉnh kích thước dựa trên ZoomScale
// Tăng kích thước lên 1.5x để robot to hơn
double scaleFactor = 2 / ZoomScale; // Tăng kích thước hiển thị
scaleFactor = scaleFactor < 1 ? 1 : scaleFactor;
double width = RobotWidthMeters * scaleFactor;
double height = RobotLengthMeters * scaleFactor;
double x = -width / 2;
double y = -height / 2;
return (x, y, width, height);
}
private double GetNodeRadius()
{
// Kích thước node cơ bản trong world coordinates - tăng lên 1.5x
const double BaseNodeRadius = 0.15;
// Điều chỉnh theo ZoomScale tương tự robot
double scaleFactor = 1.5 / ZoomScale; // Tăng kích thước hiển thị
return BaseNodeRadius * scaleFactor;
}
private double GetNodeStrokeWidth()
{
// Stroke width cơ bản - tăng lên một chút
const double BaseStrokeWidth = 0.03;
// Điều chỉnh theo ZoomScale
double scaleFactor = 1.5 / ZoomScale;
return BaseStrokeWidth * scaleFactor;
}
private double WorldToSvgX(double worldX)
{
return worldX;
}
private double WorldToSvgY(double worldY)
{
// Flip Y axis: World Y↑ → SVG Y↓
return -worldY;
}
private string GetOriginMarkerTransform()
{
// Origin is at (MapImageOriginX, MapImageOriginY) in world coordinates (bottom-left corner of image)
// In SVG coordinates (after Y flip): (MapImageOriginX, -MapImageOriginY)
// Note: Image is rendered at (MapImageOriginX, -MapImageOriginY - MapImageHeight) in SVG
// So origin marker should be at (MapImageOriginX, -MapImageOriginY) in SVG
var x = WorldToSvgX(0);
var y = WorldToSvgY(0);
return $"translate({x}, {y})";
}
private double GetOriginMarkerSize()
{
// Marker size in world coordinates (meters)
const double BaseMarkerSize = 1; // 1 meter
double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant
return BaseMarkerSize * scaleFactor;
}
private double GetOriginMarkerStrokeWidth()
{
// Stroke width in world coordinates
const double BaseStrokeWidth = 0.05; // 5cm
double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant
return BaseStrokeWidth * scaleFactor;
}
public void OnMonitorDataUpdated()
{
// Auto-follow robot when MonitorData changes
if (AutoFollowRobot && !IsPanning && MonitorData?.RobotPosition != null)
{
UpdateViewToFollowRobot();
}
}
private void UpdateViewToFollowRobot()
{
if (MonitorData?.RobotPosition == null) return;
// Center view on robot
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
StateHasChanged();
}
public void UpdatePath()
{
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
{
var path = MonitorData.EdgeStates.Select(e => new EdgeStateDto
{
Degree = e.Degree,
StartX = WorldToSvgX(e.StartX),
StartY = WorldToSvgY(e.StartY),
EndX = WorldToSvgX(e.EndX),
EndY = WorldToSvgY(e.EndY),
ControlPoint1X = WorldToSvgX(e.ControlPoint1X),
ControlPoint1Y = WorldToSvgY(e.ControlPoint1Y),
ControlPoint2X = WorldToSvgX(e.ControlPoint2X),
ControlPoint2Y = WorldToSvgY(e.ControlPoint2Y),
}).ToList();
var inPath = $"M {path[0].StartX} {path[0].StartY}";
for (int i = 0; i < path.Count; i++)
{
if (path[i].Degree == 1) inPath = $"{inPath} L {path[i].EndX} {path[i].EndY}";
else if (path[i].Degree == 2) inPath = $"{inPath} Q {path[i].ControlPoint1X} {path[i].ControlPoint1Y} {path[i].EndX} {path[i].EndY}";
else inPath = $"{inPath} C {path[i].ControlPoint1X} {path[i].ControlPoint1Y}, {path[i].ControlPoint2X} {path[i].ControlPoint2Y}, {path[i].EndX} {path[i].EndY}";
}
PathView = inPath;
PathIsNot = "visible";
}
else
{
PathView = "";
PathIsNot = "hidden";
}
StateHasChanged();
}
private async Task ZoomIn()
{
ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * 1.15);
await Task.CompletedTask;
StateHasChanged();
}
private async Task ZoomOut()
{
ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * 0.85);
await Task.CompletedTask;
StateHasChanged();
}
private async Task ResetView()
{
// Reset về zoom ban đầu (2.5x) khi có robot position
if (MonitorData?.RobotPosition != null)
{
ZoomScale = 2.5;
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
}
else
{
ZoomScale = 1.0;
TranslateX = SvgWidth / 2;
TranslateY = SvgHeight / 2;
}
await Task.CompletedTask;
StateHasChanged();
}
private void HandleMouseDown(MouseEventArgs e)
{
IsPanning = true;
PanStartX = e.ClientX - TranslateX;
PanStartY = e.ClientY - TranslateY;
}
private async Task HandleMouseMove(MouseEventArgs e)
{
// Calculate world coordinates of mouse
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("robotMonitor.getElementBoundingRect", SvgRef);
double mouseX = e.ClientX - svgRect.X;
double mouseY = e.ClientY - svgRect.Y;
// Convert to world coordinates
// World X = (mouseX - TranslateX) / (ZoomScale * BASE_PIXELS_PER_METER)
MouseWorldX = (mouseX - TranslateX) / (ZoomScale * BASE_PIXELS_PER_METER);
// World Y = -(mouseY - TranslateY) / (ZoomScale * BASE_PIXELS_PER_METER) (flip Y axis)
MouseWorldY = -(mouseY - TranslateY) / (ZoomScale * BASE_PIXELS_PER_METER);
if (IsPanning)
{
TranslateX = e.ClientX - PanStartX;
TranslateY = e.ClientY - PanStartY;
}
StateHasChanged();
}
private void HandleMouseUp(MouseEventArgs e)
{
IsPanning = false;
}
private void HandleMouseLeave(MouseEventArgs e)
{
IsPanning = false;
MouseWorldX = null;
MouseWorldY = null;
StateHasChanged();
}
private async Task HandleWheel(WheelEventArgs e)
{
const double zoomFactor = 0.1;
double oldZoom = ZoomScale;
if (e.DeltaY < 0)
ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor));
else
ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor));
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
// Zoom at mouse position
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("robotMonitor.getElementBoundingRect", SvgRef);
double mouseX = e.ClientX - svgRect.X;
double mouseY = e.ClientY - svgRect.Y;
// Calculate world coordinates at mouse position
double worldX = (mouseX - TranslateX) / (oldZoom * BASE_PIXELS_PER_METER);
double worldY = -(mouseY - TranslateY) / (oldZoom * BASE_PIXELS_PER_METER);
// Adjust translate to keep world point under mouse
TranslateX = mouseX - worldX * ZoomScale * BASE_PIXELS_PER_METER;
TranslateY = mouseY + worldY * ZoomScale * BASE_PIXELS_PER_METER;
StateHasChanged();
}
}

View File

@@ -0,0 +1,94 @@
.robot-monitor-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
}
.toolbar {
display: flex;
align-items: center;
padding: 8px 16px;
background-color: #2d2d2d;
border-bottom: 1px solid #444;
gap: 8px;
min-height: 48px;
}
.action-button {
padding: 8px;
border: none;
background: #3d3d3d;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
transition: background-color 0.2s;
}
.action-button:hover {
background: #4d4d4d;
}
.action-button:active {
background: #5d5d5d;
}
.icon-button {
font-size: 20px;
color: #fff;
}
.robot-position-info {
color: #fff;
font-size: 14px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 4px 12px;
background-color: #3d3d3d;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.mouse-position-info {
color: #fff;
font-size: 14px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 4px 12px;
background-color: #3d3d3d;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.auto-follow-control {
display: flex;
align-items: center;
padding: 4px 8px;
background-color: #3d3d3d;
border-radius: 4px;
}
.svg-container {
flex: 1;
overflow: hidden;
position: relative;
background-color: #808080;
}
.svg-container svg {
width: 100%;
height: 100%;
cursor: grab;
background-color: #808080;
}
.svg-container svg:active {
cursor: grabbing;
}

View File

@@ -1,18 +0,0 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,430 @@
@page "/dashboard"
@using RobotApp.Client.Services
@using RobotApp.VDA5050.State
@using MudBlazor
@implements IDisposable
@inject RobotStateClient RobotStateClient
@rendermode InteractiveWebAssemblyNoPrerender
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4" Style="overflow-y:auto">
<!-- Header Dashboard -->
<MudPaper Class="pa-6 mb-4 d-flex align-center justify-space-between" Elevation="3">
<div>
<MudText Typo="Typo.h3" Class="mb-2">Robot Dashboard</MudText>
@if (CurrentState != null)
{
<MudText Typo="Typo.subtitle1">
<MudIcon Icon="@Icons.Material.Filled.Memory" Class="mr-2" />
@CurrentState.Version •
<MudIcon Icon="@Icons.Material.Filled.Business" Class="mr-2 ml-4" />
@CurrentState.Manufacturer •
<MudIcon Icon="@Icons.Material.Filled.Tag" Class="mr-2 ml-4" />
@CurrentState.SerialNumber
</MudText>
}
else
{
<MudText Typo="Typo.subtitle1">
<MudIcon Icon="@Icons.Material.Filled.Sync" Class="mr-2" />
Connecting to robot...
</MudText>
}
</div>
@if (CurrentState != null)
{
<MudChip T="string"
Icon="@(IsConnected ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
Size="Size.Large"
Color="@(IsConnected ? Color.Success : Color.Error)"
Variant="Variant.Filled"
Class="px-6 py-4 text-white"
Style="font-weight: bold; font-size: 1.1rem;">
@(IsConnected ? "ONLINE" : "OFFLINE")
</MudChip>
}
</MudPaper>
@{
var msg = CurrentState ?? EmptyState;
}
<!-- Thông tin header state -->
<MudPaper Class="pa-5 mb-6 rounded-lg" Elevation="4">
<MudGrid Spacing="4" Class="align-center">
<MudItem xs="12" sm="4">
<MudText Typo="Typo.caption" Color="Color.Tertiary">Header ID</MudText>
<MudText Typo="Typo.h6">@msg.HeaderId</MudText>
</MudItem>
<MudItem xs="12" sm="4">
<MudText Typo="Typo.caption" Color="Color.Tertiary">Timestamp (UTC)</MudText>
<MudText Typo="Typo.h6">@msg.Timestamp</MudText>
</MudItem>
<MudItem xs="12" sm="2">
<MudText Typo="Typo.caption" Color="Color.Tertiary">Version</MudText>
<MudText Typo="Typo.h6">@msg.Version</MudText>
</MudItem>
<MudItem xs="12" sm="2" Class="d-flex justify-center">
<MudText Typo="Typo.caption" Color="Color.Tertiary">Order Update ID</MudText>
<MudChip T="string" Color="Color.Primary" Variant="@Variant.Filled" Class="mt-2">
@msg.OrderUpdateId
</MudChip>
</MudItem>
</MudGrid>
</MudPaper>
<MudGrid Spacing="4">
<!-- POSITION + VELOCITY -->
<MudItem xs="12" md="6" lg="4">
<MudCard Elevation="6" Class="h-100 rounded-lg">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Class="mr-2" />
Position & Velocity
</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudChip T="string"
Size="Size.Small"
Color="@(msg.NewBaseRequest ? Color.Success : Color.Error)"
Variant="@Variant.Filled">
NewBase: @(msg.NewBaseRequest ? "TRUE" : "FALSE")
</MudChip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent Class="pa-4">
<MudGrid Spacing="3">
<MudItem xs="6">
<MudText>X: <strong>@msg.AgvPosition.X.ToString("F2")</strong> m</MudText>
<MudText>Y: <strong>@msg.AgvPosition.Y.ToString("F2")</strong> m</MudText>
<MudText>θ: <strong>@msg.AgvPosition.Theta.ToString("F2")</strong> rad</MudText>
</MudItem>
<MudItem xs="6">
<MudText>Vx: <strong>@msg.Velocity.Vx.ToString("F2")</strong> m/s</MudText>
<MudText>Vy: <strong>@msg.Velocity.Vy.ToString("F2")</strong> m/s</MudText>
<MudText>Ω: <strong>@msg.Velocity.Omega.ToString("F3")</strong> rad/s</MudText>
</MudItem>
</MudGrid>
<MudDivider Class="my-4" />
<div class="d-flex justify-space-between align-center">
<MudChip T="string"
Size="Size.Small"
Color="@(msg.AgvPosition.PositionInitialized ? Color.Success : Color.Error)"
Variant="@Variant.Filled">
@(msg.AgvPosition.PositionInitialized ? "Initialized" : "Not Initialized")
</MudChip>
<MudText Typo="Typo.caption">
Deviation: <strong>@msg.AgvPosition.DeviationRange</strong>
</MudText>
</div>
<MudProgressLinear Value="@(msg.AgvPosition.LocalizationScore * 100)"
Color="Color.Success"
Class="mt-4 rounded"
Style="height: 10px;" />
<MudText Typo="Typo.caption" Class="mt-2 text-center">
Localization Score: <strong>@(msg.AgvPosition.LocalizationScore * 100)%</strong>
</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<!-- BATTERY -->
<MudItem xs="12" md="6" lg="4">
<MudCard Elevation="6" Class="h-100 rounded-lg">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.BatteryFull" Class="mr-2" />
Battery Status
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pa-6">
<MudProgressLinear Value="@msg.BatteryState.BatteryCharge"
Size="Size.Large"
Rounded="true"
Striped="true"
Color="@(msg.BatteryState.BatteryCharge > 50 ? Color.Success :
msg.BatteryState.BatteryCharge > 20 ? Color.Warning : Color.Error)"
Class="mb-4"
Style="height: 28px;" />
<!-- Phần mới: % pin và trạng thái Charging/Discharging cùng dòng -->
<div class="d-flex align-center justify-space-between mb-6">
<MudText Typo="Typo.h3" Class="mb-0">
@msg.BatteryState.BatteryCharge<span class="mud-typography-h4">%</span>
</MudText>
<MudChip T="string"
Icon="@(msg.BatteryState.Charging ? Icons.Material.Filled.Bolt : Icons.Material.Filled.PowerOff)"
Size="Size.Large"
Color="@(msg.BatteryState.Charging ? Color.Info : Color.Default)"
Variant="@Variant.Filled"
Class="px-5 py-3">
@(msg.BatteryState.Charging ? "Charging" : "Discharging")
</MudChip>
</div>
<!-- Các thông tin phụ -->
<MudGrid Spacing="3">
<MudItem xs="4">
<MudText Typo="Typo.caption">Voltage</MudText>
<MudText Typo="Typo.h6">@msg.BatteryState.BatteryVoltage:F1 V</MudText>
</MudItem>
<MudItem xs="4">
<MudText Typo="Typo.caption">Health (SOH)</MudText>
<MudText Typo="Typo.h6">@msg.BatteryState.BatteryHealth%</MudText>
</MudItem>
<MudItem xs="4">
<MudText Typo="Typo.caption">Reach</MudText>
<MudText Typo="Typo.h6">@((int)msg.BatteryState.Reach) m</MudText>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudItem>
<!-- ORDER & PATH -->
<MudItem xs="12" md="6" lg="4">
<MudCard Elevation="6" Class="h-100 rounded-lg">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Navigation" Class="mr-2" />
Order & Path
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pa-4">
<MudText>Order ID: <strong>@(msg.OrderId ?? "—")</strong></MudText>
<MudText>Update ID: <strong>@msg.OrderUpdateId</strong></MudText>
<MudDivider Class="my-4" />
<MudText>Last Node: <strong>@msg.LastNodeId</strong> <span class="mud-typography-caption">(Seq: @msg.LastNodeSequenceId)</span></MudText>
<MudText>Distance since last: <strong>@msg.DistanceSinceLastNode m</strong></MudText>
<MudDivider Class="my-4" />
@{
var nodeReleased = msg.NodeStates?.Count(n => n.Released) ?? 0;
var nodeTotal = msg.NodeStates?.Length ?? 0;
var edgeReleased = msg.EdgeStates?.Count(e => e.Released) ?? 0;
var edgeTotal = msg.EdgeStates?.Length ?? 0;
}
<div class="d-flex flex-wrap gap-3 mt-3">
<MudChip T="string" Color="Color.Primary" Variant="@Variant.Filled">Nodes: @nodeReleased/@nodeTotal</MudChip>
<MudChip T="string" Color="Color.Secondary" Variant="@Variant.Filled">Edges: @edgeReleased/@edgeTotal</MudChip>
<MudChip T="string" Size="Size.Small" Color="@(msg.Driving? Color.Success: Color.Default)" Variant="@Variant.Filled">
@(msg.Driving ? "DRIVING" : "STOPPED")
</MudChip>
<MudChip T="string" Size="Size.Small" Color="@(msg.Paused? Color.Warning: Color.Success)" Variant="@Variant.Filled">
@(msg.Paused ? "PAUSED" : "ACTIVE")
</MudChip>
</div>
</MudCardContent>
</MudCard>
</MudItem>
<!-- ERRORS + INFORMATION -->
<MudItem xs="12" lg="6">
<MudCard Elevation="6" Class="h-100 rounded-lg">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.WarningAmber" Class="mr-2" />
Errors & Information
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudTable Items="@MessageRows"
Dense="true"
Hover="true"
Bordered="true"
FixedHeader="true"
Height="250px">
<ColGroup>
<col style="width:30%" />
<col style="width:15%" />
<col />
</ColGroup>
<HeaderContent>
<MudTh><MudText Typo="Typo.subtitle2">Type</MudText></MudTh>
<MudTh><MudText Typo="Typo.subtitle2">Level</MudText></MudTh>
<MudTh><MudText Typo="Typo.subtitle2">Description</MudText></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudText Color="@(context.IsError? Color.Error: Color.Info)" Typo="Typo.body1">
<strong>@context.Type</strong>
</MudText>
</MudTd>
<MudTd>
<MudChip T="string"
Size="Size.Small"
Color="@(context.Level.Contains("ERROR", StringComparison.OrdinalIgnoreCase) ? Color.Error :
context.Level.Contains("WARN", StringComparison.OrdinalIgnoreCase) ? Color.Warning : Color.Info)"
Variant="@Variant.Filled">
@context.Level
</MudChip>
</MudTd>
<MudTd>
<MudText Typo="Typo.body2" Class="text-truncate" Style="max-width: 300px;">
@context.Description
</MudText>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Class="mx-4 my-8" Variant="@Variant.Text">
<MudText>No errors or information messages</MudText>
</MudAlert>
</NoRecordsContent>
</MudTable>
</MudCardContent>
</MudCard>
</MudItem>
<!-- ACTIONS -->
<MudItem xs="12" md="6" lg="3">
<MudCard Elevation="6" Class="h-100 rounded-lg">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Settings" Class="mr-2" />
Active Actions
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudTable Items="msg.ActionStates"
Dense="true"
Hover="true"
Bordered="true"
FixedHeader="true"
Height="220px">
<HeaderContent>
<MudTh>Action</MudTh>
<MudTh>ID</MudTh>
<MudTh Style="text-align:right">Status</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd><MudText Typo="Typo.body2" Class="text-truncate">@context.ActionType</MudText></MudTd>
<MudTd><MudText Typo="Typo.caption">@context.ActionId</MudText></MudTd>
<MudTd Style="text-align:right">
<MudChip T="string"
Size="Size.Small"
Color="@(context.ActionStatus == "RUNNING" ? Color.Info :
context.ActionStatus == "FINISHED" ? Color.Success :
context.ActionStatus == "FAILED" ? Color.Error : Color.Default)"
Variant="@Variant.Filled">
@context.ActionStatus
</MudChip>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText Class="pa-8 text-center" Color="Color.Secondary">No active actions</MudText>
</NoRecordsContent>
</MudTable>
</MudCardContent>
</MudCard>
</MudItem>
<!-- SAFETY -->
<MudItem xs="12" md="6" lg="3">
<MudCard Elevation="6" Class="h-100 rounded-lg">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Security" Class="mr-2" />
Safety State
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pa-6 d-flex flex-column gap-4">
<MudChip T="string"
Icon="@(msg.SafetyState.EStop == "NONE" ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.ErrorOutline)"
Size="Size.Large"
Color="@(msg.SafetyState.EStop == "NONE" ? Color.Success : Color.Error)"
Variant="@Variant.Filled"
Class="py-6">
E-STOP: @msg.SafetyState.EStop
</MudChip>
<MudChip T="string"
Icon="@(msg.SafetyState.FieldViolation ? Icons.Material.Filled.Warning : Icons.Material.Filled.Shield)"
Size="Size.Large"
Color="@(msg.SafetyState.FieldViolation ? Color.Error : Color.Success)"
Variant="@Variant.Filled"
Class="py-6">
Field Violation: @(msg.SafetyState.FieldViolation ? "YES" : "NO")
</MudChip>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
@code {
private static readonly StateMsg EmptyState = new()
{
};
private StateMsg? CurrentState;
private bool IsConnected;
private List<MessageRow> MessageRows = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!firstRender) return;
RobotStateClient.OnStateReceived += OnRobotStateReceived;
RobotStateClient.OnRobotConnectionChanged += OnRobotConnectionChanged;
await RobotStateClient.StartAsync();
CurrentState = RobotStateClient.GetLatestState();
IsConnected = RobotStateClient.IsRobotConnected;
UpdateMessageRows();
}
private void OnRobotConnectionChanged(bool connected)
{
IsConnected = connected;
StateHasChanged();
}
private void OnRobotStateReceived(string serialNumber, StateMsg state)
{
CurrentState = state;
UpdateMessageRows();
StateHasChanged();
}
private void UpdateMessageRows()
{
MessageRows.Clear();
if (CurrentState?.Errors != null)
{
foreach (var err in CurrentState.Errors)
{
MessageRows.Add(new MessageRow(err.ErrorType ?? "-", err.ErrorLevel ?? "ERROR", err.ErrorDescription ?? "", true));
}
}
if (CurrentState?.Information != null)
{
foreach (var info in CurrentState.Information)
{
MessageRows.Add(new MessageRow(info.InfoType ?? "-", info.InfoLevel ?? "INFO", info.InfoDescription ?? "", false));
}
}
}
public void Dispose()
{
RobotStateClient.OnStateReceived -= OnRobotStateReceived;
RobotStateClient.OnRobotConnectionChanged -= OnRobotConnectionChanged;
}
private record MessageRow(string Type, string Level, string Description, bool IsError);
}

View File

@@ -1,4 +0,0 @@
@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<h1>Welcome to RobotApp!</h1>

View File

@@ -0,0 +1,182 @@
@page "/logs"
@rendermode InteractiveWebAssemblyNoPrerender
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using RobotApp.Client.Models
@inject IJSRuntime JSRuntime
@inject HttpClient Http
@inject IConfiguration Configuration
@inject ISnackbar Snackbar
<PageTitle>Logs</PageTitle>
<div class="w-100 h-100 d-flex flex-column">
<div class="d-flex flex-row align-items-center justify-content-between" style="border-bottom: 1px solid silver">
<MudTextField Class="mt-1 ms-2" T="string" Value="FilterLog" Adornment="Adornment.End" ValueChanged="OnSearch" AdornmentIcon="@Icons.Material.Filled.Search"
IconSize="Size.Medium" Variant="Variant.Outlined" Margin="Margin.Dense" AdornmentColor="Color.Secondary" Label="Search"></MudTextField>
<MudSpacer />
<div class="m-1 d-flex flex-row">
<MudDatePicker Class="mx-4" Label="Date" Date="DateLog" DateChanged="OnDateChanged" MaxDate="DateTime.Today" Variant="Variant.Outlined" Color="Color.Primary"
ShowToolbar="false" Margin="Margin.Dense" AdornmentColor="Color.Primary" />
<MudTooltip Text="Export">
<MudFab Class="mt-2" Color="Color.Info" StartIcon="@Icons.Material.Filled.ImportExport" Size="Size.Small" OnClick="ExportLogs" />
</MudTooltip>
<MudTooltip Text="Refresh">
<MudFab Class="mx-4 mt-2" StartIcon="@Icons.Material.Filled.Refresh" Color="Color.Primary" Size="Size.Small" OnClick="LoadLogs" />
</MudTooltip>
</div>
</div>
<div class="flex-grow-1 mt-2 ms-2 position-relative" style="background-color: rgba(0, 0, 0, 0);">
<MudOverlay Visible="IsLoading" DarkBackground="true" Absolute="true">
<MudProgressCircular Color="Color.Info" Indeterminate="true" />
</MudOverlay>
<div class="h-100 w-100 position-relative">
<div class="log-container" @ref="LogContainerRef">
@if (ShowRawLog)
{
<div class="d-flex justify-content-center my-3">
<div><MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => ShowRawLog = false)">Normal</MudButton></div>
</div>
<big style="font-size: 14px;">
@foreach (var log in ShowLogs)
{
@log <br />
}
</big>
}
else
{
@if (SearchLogs.Count < ShowLogs.Count)
{
<div class="d-flex justify-content-center my-3">
<div><MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => ShowRawLog = true)">Raw log</MudButton></div>
</div>
}
@foreach (var log in SearchLogs)
{
<div class="log">
<span class="log-head @log.BackgroundClass">
@log.Time <span class="log-level">@log.Level</span>
</span>
<span>@log.Message</span>
@if (log.HasException)
{
<br />
<pre class="log-exception">
@log.Exception
</pre>
}
</div>
}
}
</div>
</div>
</div>
</div>
<script>
window.ScrollToBottom = (element) => {
if (element) {
element.scrollTop = element.scrollHeight;
}
};
</script>
@code {
private DateTime DateLog = DateTime.Today;
private bool IsLoading;
private readonly List<string> ShowLogs = new();
private readonly List<LoggerModel> SearchLogs = new();
private ElementReference LogContainerRef { get; set; }
private bool ShowRawLog { get; set; }
private string? FilterLog { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!firstRender) return;
await LoadLogs();
}
private async Task LoadLogs()
{
try
{
IsLoading = true;
ShowLogs.Clear();
StateHasChanged();
var logs = await Http.GetFromJsonAsync<IEnumerable<string>>($"api/LogsManager?date={DateLog}");
ShowLogs.AddRange(logs ?? []);
IsLoading = false;
StateHasChanged();
await ReloadLogs();
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
return;
}
}
private async Task ReloadLogs()
{
IsLoading = true;
SearchLogs.Clear();
StateHasChanged();
foreach (var line in ShowLogs.Where(log => string.IsNullOrEmpty(FilterLog) || log.Contains(FilterLog)).TakeLast(2000))
{
try
{
var log = System.Text.Json.JsonSerializer.Deserialize<LoggerModel>(line);
if (log is not null) SearchLogs.Add(log);
}
catch (System.Text.Json.JsonException)
{
continue;
}
}
IsLoading = false;
StateHasChanged();
await JSRuntime.InvokeVoidAsync("ScrollToBottom", LogContainerRef);
}
private async Task OnSearch(string text)
{
FilterLog = text;
await ReloadLogs();
}
private async Task OnDateChanged(DateTime? date)
{
if (date is not null && date.HasValue)
{
DateLog = date.Value;
await LoadLogs();
}
}
private async Task ExportLogs()
{
try
{
var fileContent = await Http.GetFromJsonAsync<IEnumerable<string>>($"api/LogsManager?date={DateLog}");
var formattedContent = string.Join("\n", fileContent ?? []);
var fileName = $"LogsManager_{DateLog.ToShortDateString()}.txt";
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, formattedContent, "text/plain");
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải file: {ex.Message}", Severity.Warning);
}
}
}

View File

@@ -0,0 +1,38 @@
.log-container {
height: 100%;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
position: absolute;
top: 0px;
left: 0px;
display: flex;
flex-direction: column;
}
.log {
word-wrap: break-word;
line-height: 18px;
margin-bottom: 12px;
}
.log-logger {
color: rgba(0, 0, 0, 0.3);
font-size: 12px;
}
.log-level {
display: inline-block;
width: 60px;
}
.log-head {
border-radius: 3px;
padding: 2px 5px;
}
.log-exception {
line-height: 16px;
margin-left: 30px;
color: crimson;
}

View File

@@ -0,0 +1,23 @@
@page "/maps-manager"
@rendermode InteractiveWebAssemblyNoPrerender
<PageTitle>Map Manager</PageTitle>
<div class="d-flex w-100 h-100 p-2 overflow-hidden flex-row">
<div class="me-4 d-flex flex-column" style="height: 100%; width: 40%">
<div class="mb-4" style="height: 50%; width: 100%">
<RobotApp.Client.Pages.Components.Mapping.MapTable />
</div>
<div class="flex-grow-1" style="height: 50%; width: 100%">
<RobotApp.Client.Pages.Components.Mapping.RobotInfomation />
</div>
</div>
<div class="flex-grow-1 h-100" style="width: 60%">
<RobotApp.Client.Pages.Components.Mapping.MapView />
</div>
</div>
@code {
private List<MapDto> MapsShow = [];
}

View File

@@ -0,0 +1,123 @@
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween"
Class="mb-4 flex-shrink-0">
<MudText Typo="Typo.h6">🔗 Edges</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddEdgeAsync">
Add Edge
</MudButton>
</MudStack>
<div class="flex-grow-1" style="overflow:auto;">
<MudExpansionPanels MultiExpansion>
@foreach (var edge in Order.Edges)
{
<MudExpansionPanel @key="edge">
<!-- ================= HEADER ================= -->
<TitleContent>
<div class="d-flex align-center justify-space-between w-100">
<!-- LEFT: Edge information -->
<div class="d-flex align-center gap-3">
<MudText Typo="Typo.subtitle1" Class="fw-bold">
@edge.EdgeId
</MudText>
<MudChip T="string"
Size="Size.Small"
Color="Color.Info"
Variant="Variant.Outlined">
@edge.StartNodeId → @edge.EndNodeId
</MudChip>
</div>
<!-- RIGHT: Delete -->
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => RemoveEdgeAsync(edge))" />
</div>
</TitleContent>
<!-- ================= BODY ================= -->
<ChildContent>
<MudGrid Spacing="3">
<!-- Edge ID -->
<MudItem xs="12">
<MudTextField Value="@edge.EdgeId"
ValueChanged="@((string v) => SetValue(() => edge.EdgeId = v))"
Immediate="true"
Label="Edge ID" />
</MudItem>
<!-- Start Node -->
<MudItem xs="12">
<MudSelect T="string"
Value="@edge.StartNodeId"
ValueChanged="@((string v) => SetValue(() => edge.StartNodeId = v))"
Dense
Label="Start Node"
Required="true">
@foreach (var n in Order.Nodes)
{
<MudSelectItem Value="@n.NodeId">
@n.NodeId
</MudSelectItem>
}
</MudSelect>
</MudItem>
<!-- End Node -->
<MudItem xs="12">
<MudSelect T="string"
Value="@edge.EndNodeId"
ValueChanged="@((string v) => SetValue(() => edge.EndNodeId = v))"
Dense
Label="End Node"
Required="true">
@foreach (var n in Order.Nodes)
{
<MudSelectItem Value="@n.NodeId">
@n.NodeId
</MudSelectItem>
}
</MudSelect>
</MudItem>
</MudGrid>
</ChildContent>
</MudExpansionPanel>
}
</MudExpansionPanels>
</div>
</MudPaper>
@code {
[Parameter] public OrderMessage Order { get; set; } = default!;
[Parameter] public EventCallback OnAddEdge { get; set; }
[Parameter] public EventCallback<VDA5050.Order.Edge> OnRemoveEdge { get; set; }
[Parameter] public EventCallback OnOrderChanged { get; set; }
private async Task SetValue(System.Action setter)
{
setter();
await OnOrderChanged.InvokeAsync();
}
private async Task AddEdgeAsync()
{
await OnAddEdge.InvokeAsync();
await OnOrderChanged.InvokeAsync();
}
private async Task RemoveEdgeAsync(VDA5050.Order.Edge edge)
{
await OnRemoveEdge.InvokeAsync(edge);
await OnOrderChanged.InvokeAsync();
}
}

View File

@@ -0,0 +1,201 @@
@inherits MudComponentBase
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">Edit Node: @Node.NodeId</MudText>
</TitleContent>
<DialogContent>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudTextField @bind-Value="Node.NodeId" Label="Node ID" Required="true" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double" @bind-Value="Node.NodePosition.X" Label="X" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double" @bind-Value="Node.NodePosition.Y" Label="Y" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double" @bind-Value="Node.NodePosition.Theta" Label="Theta (rad)" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double" @bind-Value="Node.NodePosition.AllowedDeviationXY"
Label="Allowed Dev XY" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double" @bind-Value="Node.NodePosition.AllowedDeviationTheta"
Label="Allowed Dev Theta" />
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="Node.NodePosition.MapId" Label="Map ID" />
</MudItem>
<MudItem xs="12">
<MudDivider Class="my-4" />
<MudText Typo="Typo.subtitle1" Class="mb-3">Actions</MudText>
@foreach (var act in Node.Actions)
{
<MudPaper Class="pa-3 mb-3" Outlined="true">
<MudGrid Spacing="3">
<MudItem xs="10">
<MudItem xs="12">
<MudSelect T="string"
Label="Action Type"
Dense="true"
Required="true"
@bind-Value="act.ActionType">
@foreach (var at in Enum.GetValues<ActionType>())
{
<MudSelectItem Value="@at.ToString()">
@at
</MudSelectItem>
}
</MudSelect>
</MudItem>
</MudItem>
<MudItem xs="12">
<MudSelect T="string" @bind-Value="act.BlockingType" Label="Blocking Type">
<MudSelectItem Value="@("NONE")">NONE</MudSelectItem>
<MudSelectItem Value="@("SOFT")">SOFT</MudSelectItem>
<MudSelectItem Value="@("HARD")">HARD</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="act.ActionId" Label="Action ID" />
</MudItem>
</MudGrid>
<MudText Typo="Typo.caption" Class="mt-3 mb-2">Action Parameters</MudText>
@{
var parameters = act.ActionParameters ?? Array.Empty<ActionParameter>();
}
@foreach (var p in parameters.Cast<UiActionParameter>().ToList())
{
var param = p; // capture cho lambda
<MudGrid Class="mt-1">
<MudItem xs="5">
<MudTextField @bind-Value="param.Key" Label="Key" />
</MudItem>
<MudItem xs="5">
<MudTextField @bind-Value="param.ValueString" Label="Value" />
</MudItem>
<MudItem xs="2">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => RemoveParameter(act, param))" />
</MudItem>
</MudGrid>
}
<MudButton Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add"
Class="mt-3"
OnClick="@(() => AddParameter(act))">
Add Parameter
</MudButton>
<MudDivider Class="my-3" />
<MudButton Size="Size.Small"
Color="Color.Error"
Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.Delete"
OnClick="@(() => RemoveAction(act))">
Remove Action
</MudButton>
</MudPaper>
}
<MudButton Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddNewAction">
Add Action
</MudButton>
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public Node Node { get; set; } = default!;
private void Cancel() => MudDialog.Cancel();
private void Submit() => MudDialog.Close(DialogResult.Ok(true));
private void RemoveAction(VDA5050.InstantAction.Action actToRemove)
{
Node.Actions = Node.Actions
.Where(a => a != actToRemove)
.ToArray();
}
private void AddNewAction()
{
Node.Actions = Node.Actions
.Append(new VDA5050.InstantAction.Action
{
ActionId = Guid.NewGuid().ToString(),
ActionType = ActionType.startPause.ToString(),
BlockingType = "NONE",
ActionParameters = Array.Empty<ActionParameter>()
})
.ToArray();
}
private void AddParameter(VDA5050.InstantAction.Action act)
{
var newParam = new UiActionParameter();
if (act.ActionParameters == null || act.ActionParameters.Length == 0)
{
act.ActionParameters = new[] { newParam };
}
else
{
var list = act.ActionParameters.ToList();
list.Add(newParam);
act.ActionParameters = list.ToArray();
}
}
private void RemoveParameter(VDA5050.InstantAction.Action act, UiActionParameter paramToRemove)
{
if (act.ActionParameters == null || act.ActionParameters.Length == 0)
return;
act.ActionParameters = act.ActionParameters
.Where(p => p != paramToRemove) // so sánh reference
.ToArray();
}
// UiActionParameter vẫn giữ như cũ trong trang chính
public class UiActionParameter : ActionParameter
{
[JsonIgnore]
public string ValueString
{
get => Value?.ToString() ?? "";
set => Value = value;
}
}
}

View File

@@ -0,0 +1,199 @@
@using System.Text.Json
@using MudBlazor
@using Microsoft.AspNetCore.Components.Forms
<MudDialog>
<!-- ================= TITLE ================= -->
<TitleContent>
<MudStack Row AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.UploadFile"
Color="Color.Primary" />
<MudText Typo="Typo.h6">
Import Order JSON
</MudText>
</MudStack>
</TitleContent>
<!-- ================= CONTENT ================= -->
<DialogContent>
@if (ShowWarning)
{
<MudAlert Severity="Severity.Warning"
Variant="Variant.Outlined"
Class="mb-4">
Only valid <b>VDA 5050 Order JSON</b> is accepted.<br />
Invalid structure will be rejected.
</MudAlert>
}
<!-- ===== FILE INPUT (PURE BLAZOR) ===== -->
<div class="mb-4">
<label class="mud-button-root mud-button mud-button-outlined mud-button-outlined-primary"
style="cursor:pointer;">
<MudIcon Icon="@Icons.Material.Filled.AttachFile" Class="mr-2" />
Choose JSON file
<InputFile OnChange="OnFileSelected"
accept=".json"
style="display:none" />
</label>
</div>
<MudDivider Class="my-3" />
<!-- ===== PASTE JSON ===== -->
<MudTextField @bind-Value="JsonText"
Label="Paste Order JSON"
Lines="20"
Variant="Variant.Filled"
Immediate
Style="font-family: monospace" />
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<MudAlert Severity="Severity.Error" Class="mt-3">
@ErrorMessage
</MudAlert>
}
</DialogContent>
<!-- ================= ACTIONS ================= -->
<DialogActions>
<MudButton OnClick="Cancel">
Cancel
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ValidateAndImport">
Import
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
public IMudDialogInstance MudDialog { get; set; } = default!;
public string JsonText { get; set; } = "";
public string? ErrorMessage;
public bool ShowWarning { get; set; }
private void Cancel() => MudDialog.Cancel();
// ================= FILE HANDLER =================
private async Task OnFileSelected(InputFileChangeEventArgs e)
{
ErrorMessage = null;
ShowWarning = false;
var file = e.File;
if (file == null)
return;
if (!file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase) &&
!file.Name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
{
ShowWarning = true;
ErrorMessage = "Only .json or .txt files are supported.";
return;
}
try
{
using var stream = file.OpenReadStream(maxAllowedSize: 1_048_576);
using var reader = new StreamReader(stream);
JsonText = await reader.ReadToEndAsync();
StateHasChanged();
}
catch (Exception ex)
{
ShowWarning = true;
ErrorMessage = $"Failed to read file: {ex.Message}";
}
}
// ================= VALIDATE & IMPORT =================
private void ValidateAndImport()
{
ErrorMessage = null;
ShowWarning = false;
if (string.IsNullOrWhiteSpace(JsonText))
{
ShowWarning = true;
ErrorMessage = "JSON content is empty.";
return;
}
try
{
var order = JsonSerializer.Deserialize<OrderMsg>(JsonText, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
if(order is null)
{
ShowWarning = true;
ErrorMessage = "Can not convert file to Order message";
return;
}
ValidateOrder(order);
MudDialog.Close(DialogResult.Ok(order));
}
catch (JsonException)
{
ShowWarning = true;
ErrorMessage = "Invalid JSON format.";
}
catch (Exception ex)
{
ShowWarning = true;
ErrorMessage = ex.Message;
}
}
// ================= DOMAIN VALIDATION =================
private void ValidateOrder(OrderMsg order)
{
if (order.Nodes.Length == 0)
throw new Exception("Order must contain at least one node.");
if (order.Nodes.Length != order.Edges.Length + 1)
throw new Exception(
$"Invalid path structure: Nodes count ({order.Nodes.Length}) " +
$"must equal Edges count + 1 ({order.Edges.Length + 1})."
);
var nodeIds = order.Nodes
.Select(n => n.NodeId)
.ToHashSet(StringComparer.Ordinal);
foreach (var e in order.Edges)
{
if (string.IsNullOrWhiteSpace(e.StartNodeId) ||
string.IsNullOrWhiteSpace(e.EndNodeId))
{
throw new Exception(
$"Edge '{e.EdgeId}' must define both StartNodeId and EndNodeId."
);
}
if (!nodeIds.Contains(e.StartNodeId))
throw new Exception(
$"Edge '{e.EdgeId}' references unknown StartNodeId '{e.StartNodeId}'."
);
if (!nodeIds.Contains(e.EndNodeId))
throw new Exception(
$"Edge '{e.EdgeId}' references unknown EndNodeId '{e.EndNodeId}'."
);
}
}
}

View File

@@ -0,0 +1,131 @@
<MudPaper Class="pa-4 h-100 d-flex flex-column overflow-hidden" Elevation="2">
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween"
Class="mb-4 flex-shrink-0">
<MudText Typo="Typo.h6">Output (/order)</MudText>
<div class="d-flex gap-2">
<!-- IMPORT -->
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.UploadFile"
OnClick="OnImport">
Import JSON
</MudButton>
<!-- CANCEL -->
<MudButton Variant="Variant.Filled"
Color="@CancelButtonColor"
StartIcon="@CancelButtonIcon"
Disabled="@DisableCancel"
OnClick="OnCancel">
@CancelButtonText
</MudButton>
<!-- SEND -->
<MudButton Variant="Variant.Filled"
Color="@SendButtonColor"
StartIcon="@SendButtonIcon"
OnClick="OnSend"
Disabled="@(string.IsNullOrEmpty(OrderJson.Trim()))">
@SendButtonText
</MudButton>
<!-- COPY -->
<MudTooltip Text="@(Copied ? "Copied!" : "Copy to clipboard")">
<MudButton Variant="Variant.Filled"
Color="@(Copied ? Color.Success : Color.Primary)"
StartIcon="@(Copied ? Icons.Material.Filled.Check : Icons.Material.Filled.ContentCopy)"
OnClick="OnCopy">
@(Copied ? "Copied!" : "Copy")
</MudButton>
</MudTooltip>
</div>
</MudStack>
<div class="flex-grow-1">
<MudTextField Value="@OrderJson"
T="string"
ValueChanged="OrderJsonChange"
Variant="Variant.Filled"
Immediate=true
Lines="50"
Style="font-family: 'Roboto Mono', Consolas, monospace;
font-size: 0.85rem;
background:#1e1e1e;
color:#d4d4d4;" />
</div>
</MudPaper>
@code {
[Parameter] public string OrderJson { get; set; } = "";
[Parameter] public bool Copied { get; set; }
[Parameter] public bool? SendSuccess { get; set; }
[Parameter] public bool DisableCancel { get; set; }
[Parameter] public bool? CancelSuccess { get; set; }
[Parameter] public EventCallback<string> OrderJsonChanged { get; set; }
[Parameter] public EventCallback OnCopy { get; set; }
[Parameter] public EventCallback OnSend { get; set; }
[Parameter] public EventCallback OnImport { get; set; }
[Parameter] public EventCallback OnCancel { get; set; }
private string SendButtonText =>
SendSuccess switch
{
true => "Done",
false => "Error",
_ => "Send"
};
private Color SendButtonColor =>
SendSuccess switch
{
true => Color.Success,
false => Color.Error,
_ => Color.Success
};
private string SendButtonIcon =>
SendSuccess switch
{
true => Icons.Material.Filled.CheckCircle,
false => Icons.Material.Filled.Error,
_ => Icons.Material.Filled.Send
};
private string CancelButtonText =>
CancelSuccess switch
{
true => "Done",
false => "Error",
_ => "Cancel"
};
private Color CancelButtonColor =>
CancelSuccess switch
{
true => Color.Success,
false => Color.Error,
_ => Color.Error
};
private string CancelButtonIcon =>
CancelSuccess switch
{
true => Icons.Material.Filled.CheckCircle,
false => Icons.Material.Filled.Error,
_ => Icons.Material.Filled.Cancel
};
private void OrderJsonChange(string value)
{
OrderJson = value;
OrderJsonChanged.InvokeAsync(OrderJson);
StateHasChanged();
}
}

View File

@@ -0,0 +1,274 @@
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4 flex-shrink-0">
<MudText Typo="Typo.h6">📍 Nodes</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddNodeAsync">
Add Node
</MudButton>
</MudStack>
<div class="flex-grow-1" style="overflow:auto;">
<MudExpansionPanels MultiExpansion>
@foreach (var node in Order.Nodes)
{
<MudExpansionPanel @key="node.NodeId">
<TitleContent>
<div class="d-flex align-center justify-space-between w-100">
<MudText Typo="Typo.subtitle1" Class="fw-bold">@node.NodeId</MudText>
<div>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
Size="Size.Small"
OnClick="@(() => EditNodeAsync(node))" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => RemoveNodeAsync(node))" />
</div>
</div>
</TitleContent>
<ChildContent>
<MudGrid Spacing="3">
<!-- Node ID -->
<MudItem xs="12">
<MudTextField Value="@node.NodeId"
ValueChanged="@((string v) => SetValue(() => node.NodeId = v))"
Immediate="true"
Label="Node ID" />
</MudItem>
<!-- Sequence -->
@* <MudItem xs="12">
<MudNumericField T="int"
Value="@node.SequenceId"
ValueChanged="@((int v) => SetValue(() => node.SequenceId = v))"
Immediate="true"
Label="Sequence ID" />
</MudItem> *@
<!-- Released -->
@* <MudItem xs="12">
<MudSwitch T="bool"
Checked="@node.Released"
CheckedChanged="@((bool v) => SetValue(() => node.Released = v))"
Label="Released" />
</MudItem> *@
<!-- Position -->
<MudItem xs="6">
<MudNumericField T="double"
Value="@node.NodePosition.X"
ValueChanged="@((double v) => SetValue(() => node.NodePosition.X = v))"
Immediate="true"
Label="X" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double"
Value="@node.NodePosition.Y"
ValueChanged="@((double v) => SetValue(() => node.NodePosition.Y = v))"
Immediate="true"
Label="Y" />
</MudItem>
@* <MudItem xs="12">
<MudTextField Value="@node.NodePosition.MapId"
ValueChanged="@((string v) => SetValue(() => node.NodePosition.MapId = v))"
Immediate="true"
Label="Map ID" />
</MudItem> *@
@* <MudItem xs="6">
<MudNumericField T="double"
Value="@node.NodePosition.Theta"
ValueChanged="@((double v) => SetValue(() => node.NodePosition.Theta = v))"
Immediate="true"
Label="Theta (rad)" />
</MudItem> *@
@* <MudItem xs="6">
<MudNumericField T="double"
Value="@node.NodePosition.AllowedDeviationXY"
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationXY = v))"
Immediate="true"
Label="Allowed Dev XY" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double"
Value="@node.NodePosition.AllowedDeviationTheta"
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationTheta = v))"
Immediate="true"
Label="Allowed Dev Theta" />
</MudItem> *@
<!-- Actions -->
<MudItem xs="12">
<MudDivider Class="my-4" />
<MudText Typo="Typo.subtitle1" Class="mb-3">Actions</MudText>
@foreach (var act in node.Actions ?? Array.Empty<VDA5050.InstantAction.Action>())
{
<MudPaper Class="pa-3 mb-3" Outlined>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudSelect T="string"
Value="@act.ActionType"
ValueChanged="@((string v) => SetValue(() => act.ActionType = v))"
Dense
Label="Action Type">
@foreach (var at in Enum.GetValues<ActionType>())
{
<MudSelectItem Value="@at.ToString()">@at</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudSelect T="string"
Value="@act.BlockingType"
ValueChanged="@((string v) => SetValue(() => act.BlockingType = v))"
Label="Blocking Type">
<MudSelectItem Value="@("NONE")">NONE</MudSelectItem>
<MudSelectItem Value="@("SOFT")">SOFT</MudSelectItem>
<MudSelectItem Value="@("HARD")">HARD</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudTextField Value="@act.ActionId"
ValueChanged="@((string v) => SetValue(() => act.ActionId = v))"
Immediate="true"
Label="Action ID" />
</MudItem>
</MudGrid>
<MudText Typo="Typo.caption" Class="mt-3 mb-2">Action Parameters</MudText>
@foreach (var p in act.ActionParameters.Cast<UiActionParameter>())
{
<MudGrid Class="mt-1">
<MudItem xs="6">
<MudTextField Value="@p.Key"
ValueChanged="@((string v) => SetValue(() => p.Key = v))"
Immediate="true"
Label="Key" />
</MudItem>
<MudItem xs="6">
<MudTextField Value="@p.ValueString"
ValueChanged="@((string v) => SetValue(() => p.ValueString = v))"
Immediate="true"
Label="Value" />
</MudItem>
<MudItem xs="2">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="@(() => RemoveActionParameterAsync(act, p))" />
</MudItem>
</MudGrid>
}
<MudButton Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add"
Class="mt-3"
OnClick="@(() => AddActionParameterAsync(act))">
Add Parameter
</MudButton>
<MudDivider Class="my-3" />
<MudButton Size="Size.Small"
Color="Color.Error"
Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.Delete"
OnClick="@(() => RemoveActionAsync(node, act))">
Remove Action
</MudButton>
</MudPaper>
}
<MudButton Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(() => AddActionAsync(node))">
Add Action
</MudButton>
</MudItem>
</MudGrid>
</ChildContent>
</MudExpansionPanel>
}
</MudExpansionPanels>
</div>
</MudPaper>
@code {
[Parameter] public OrderMessage Order { get; set; } = default!;
[Parameter] public EventCallback OnAddNode { get; set; }
[Parameter] public EventCallback<Node> OnRemoveNode { get; set; }
[Parameter] public EventCallback<Node> OnEditNode { get; set; }
[Parameter] public EventCallback<Node> OnAddAction { get; set; }
[Parameter] public EventCallback<NodeActionWrapper> OnRemoveAction { get; set; }
[Parameter] public EventCallback<VDA5050.InstantAction.Action> OnAddActionParameter { get; set; }
[Parameter] public EventCallback<ActionParamWrapper> OnRemoveActionParameter { get; set; }
[Parameter] public EventCallback OnOrderChanged { get; set; }
// 🔥 helper realtime KHÔNG ambiguous
private async Task SetValue(System.Action setter)
{
setter();
await OnOrderChanged.InvokeAsync();
}
private async Task AddNodeAsync()
{
await OnAddNode.InvokeAsync();
await OnOrderChanged.InvokeAsync();
}
private async Task RemoveNodeAsync(Node node)
{
await OnRemoveNode.InvokeAsync(node);
await OnOrderChanged.InvokeAsync();
}
private async Task EditNodeAsync(Node node)
{
await OnEditNode.InvokeAsync(node);
await OnOrderChanged.InvokeAsync();
}
private async Task AddActionAsync(Node node)
{
await OnAddAction.InvokeAsync(node);
await OnOrderChanged.InvokeAsync();
}
private async Task RemoveActionAsync(Node node, VDA5050.InstantAction.Action action)
{
await OnRemoveAction.InvokeAsync(new NodeActionWrapper(node, action));
await OnOrderChanged.InvokeAsync();
}
private async Task AddActionParameterAsync(VDA5050.InstantAction.Action act)
{
await OnAddActionParameter.InvokeAsync(act);
await OnOrderChanged.InvokeAsync();
}
private async Task RemoveActionParameterAsync(VDA5050.InstantAction.Action act, ActionParameter param)
{
await OnRemoveActionParameter.InvokeAsync(new ActionParamWrapper(act, param));
await OnOrderChanged.InvokeAsync();
}
public record NodeActionWrapper(Node Node, VDA5050.InstantAction.Action Action);
public record ActionParamWrapper(VDA5050.InstantAction.Action Action, ActionParameter Parameter);
}

View File

@@ -0,0 +1,334 @@
@page "/robot-order"
@rendermode InteractiveWebAssemblyNoPrerender
@using System.Text.Json
@using System.Text.Json.Serialization
@inject IJSRuntime JS
@inject IDialogService DialogService
@inject HttpClient Http
@inject ISnackbar Snackbar
<MudMainContent Class="pa-0 ma-0">
<div style="height:100vh; overflow:hidden;">
<MudContainer MaxWidth="MaxWidth.False"
Class="pa-4"
Style="max-width:100%; height:100%; display:flex; flex-direction:column;">
<MudGrid Spacing="4" Class="flex-grow-1" Style="overflow:hidden;">
<!-- ================= LEFT ================= -->
<MudItem xs="12" md="7" Class="d-flex flex-column h-100" Style="gap:16px;">
<MudGrid Spacing="4" Class="flex-grow-1" Style="overflow:hidden;">
<MudItem xs="12" md="6" Class="h-100">
<NodesPanel Order="Order"
OnAddNode="AddNode"
OnRemoveNode="RemoveNode"
OnEditNode="OpenEditNodeDialog"
OnAddAction="AddAction"
OnRemoveAction="@(w => RemoveAction(w.Node, w.Action))"
OnAddActionParameter="AddActionParameter"
OnRemoveActionParameter="@(w => RemoveActionParameter(w.Action, w.Parameter))"
OnOrderChanged="OnOrderChanged" />
</MudItem>
<MudItem xs="12" md="6" Class="h-100">
<EdgesPanel Order="Order"
OnAddEdge="AddEdge"
OnRemoveEdge="RemoveEdge"
OnOrderChanged="OnOrderChanged" />
</MudItem>
</MudGrid>
</MudItem>
<!-- ================= RIGHT ================= -->
<MudItem xs="12" md="5" Class="h-100">
<JsonOutputPanel @bind-OrderJson="@OrderJson"
Copied="@copied"
SendSuccess="@sendSuccess"
CancelSuccess="@cancelSuccess"
OnCopy="CopyJsonToClipboard"
OnSend="SendOrderToServer"
OnImport="OpenImportDialog"
OnCancel="CancelOrder" />
</MudItem>
</MudGrid>
</MudContainer>
</div>
</MudMainContent>
@code {
// ================= STATE =================
private OrderMessage Order { get; set; } = new();
private string OrderJson = ""; // 🔥 CACHE JSON (QUAN TRỌNG)
private bool copied;
private bool? sendSuccess;
private bool? cancelSuccess;
private CancellationTokenSource? _copyCts;
// ================= INIT =================
protected override void OnInitialized()
{
RebuildOrderJson();
}
// ================= CORE FIX =================
private void RebuildOrderJson()
{
OrderJson = JsonSerializer.Serialize(
Order.ToSchemaObject(),
new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
}
private async Task OpenImportDialog()
{
var dialog = await DialogService.ShowAsync<ImportOrderDialog>(
"Import Order JSON",
new DialogOptions
{
FullWidth = true,
MaxWidth = MaxWidth.Large
});
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is OrderMsg imported)
{
Order.Import(imported);
RebuildOrderJson();
StateHasChanged();
}
}
private void OnOrderChanged()
{
RebuildOrderJson(); // 🔥 JSON luôn rebuild
StateHasChanged(); // 🔥 ép render
}
// ================= NODE =================
void AddNode()
{
Order.Nodes.Add(new Node
{
NodeId = $"NODE_{Order.Nodes.Count + 1}",
SequenceId = Order.Nodes.Count,
Released = true,
NodePosition = new VDA5050.Order.NodePosition { MapId = "MAP_01" }
});
}
void RemoveNode(Node node)
{
Order.Nodes.Remove(node);
Order.Edges.RemoveAll(e => e.StartNodeId == node.NodeId || e.EndNodeId == node.NodeId);
ResequenceNodes();
}
void ResequenceNodes()
{
for (int i = 0; i < Order.Nodes.Count; i++)
Order.Nodes[i].SequenceId = i;
}
// ================= EDGE =================
void AddEdge()
{
if (Order.Nodes.Count == 0)
return;
var start = Order.Nodes[0].NodeId;
var end = Order.Nodes.Count > 1
? Order.Nodes[1].NodeId
: start; // 👈 1 node thì start = end
Order.Edges.Add(new VDA5050.Order.Edge
{
EdgeId = $"EDGE_{Order.Edges.Count + 1}",
StartNodeId = start,
EndNodeId = end
});
}
void RemoveEdge(VDA5050.Order.Edge edge)
{
Order.Edges.Remove(edge);
}
// ================= ACTION =================
void AddAction(Node node)
{
var list = node.Actions?.ToList() ?? new();
list.Add(new VDA5050.InstantAction.Action
{
ActionId = Guid.NewGuid().ToString(),
ActionType = ActionType.startPause.ToString(),
BlockingType = "NONE",
ActionParameters = Array.Empty<ActionParameter>()
});
node.Actions = list.ToArray();
}
void RemoveAction(Node node, VDA5050.InstantAction.Action action)
{
node.Actions = node.Actions?.Where(a => a != action).ToArray() ?? [];
}
void AddActionParameter(VDA5050.InstantAction.Action act)
{
var list = (act.ActionParameters ?? []).ToList();
list.Add(new UiActionParameter());
act.ActionParameters = list.ToArray();
}
void RemoveActionParameter(VDA5050.InstantAction.Action act, ActionParameter param)
{
act.ActionParameters = act.ActionParameters?.Where(p => p != param).ToArray() ?? [];
}
// ================= SEND / COPY =================
async Task SendOrderToServer()
{
// reset trạng thái trước khi gửi
sendSuccess = null;
StateHasChanged();
try
{
var orderMsg = JsonSerializer.Deserialize<OrderMsg>(OrderJson,
new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
if (orderMsg is null)
{
Snackbar.Add("Unable to convert JSON to Order", Severity.Warning);
return;
}
if (orderMsg.Nodes.Length < 1)
{
Snackbar.Add("The order must contain at least one node (number of nodes > 0)", Severity.Warning);
return;
}
if (orderMsg.Nodes.Length - 1 != orderMsg.Edges.Length)
{
Snackbar.Add("Order must have a number of edges equal to the number of nodes minus 1", Severity.Warning);
return;
}
foreach(var edge in orderMsg.Edges)
{
if (!orderMsg.Nodes.Any(n => n.NodeId == edge.StartNodeId))
{
Snackbar.Add($"The edge {edge.EdgeId} references a startNodeId {edge.StartNodeId} that does not exist in the list of nodes", Severity.Warning);
return;
}
if (!orderMsg.Nodes.Any(n => n.NodeId == edge.EndNodeId))
{
Snackbar.Add($"The edge {edge.EdgeId} references a startNodeId {edge.EndNodeId} that does not exist in the list of nodes", Severity.Warning);
return;
}
}
var response = await Http.PostAsJsonAsync("/api/order",orderMsg);
sendSuccess = response.IsSuccessStatusCode;
}
catch(JsonException jsonEx)
{
Snackbar.Add($"Json to Order failed: {jsonEx.Message}", Severity.Warning);
}
catch (Exception ex)
{
Snackbar.Add($"Send Order failed: {ex.Message}", Severity.Warning);
sendSuccess = false;
}
StateHasChanged();
// 🔥 AUTO RESET SAU 2 GIÂY
_ = Task.Run(async () =>
{
await Task.Delay(2000);
// quay về trạng thái Send
sendSuccess = null;
await InvokeAsync(StateHasChanged);
});
}
async Task CancelOrder()
{
// reset trạng thái trước khi gửi
cancelSuccess = null;
StateHasChanged();
try
{
var res = await Http.PostAsync("/api/order/cancel", null);
cancelSuccess = res.IsSuccessStatusCode;
}
catch
{
cancelSuccess = false;
}
StateHasChanged();
// 🔥 AUTO RESET SAU 2 GIÂY
_ = Task.Run(async () =>
{
await Task.Delay(2000);
cancelSuccess = null;
await InvokeAsync(StateHasChanged);
});
}
async Task CopyJsonToClipboard()
{
_copyCts?.Cancel();
_copyCts = new();
await JS.InvokeVoidAsync("copyToClipboardFallback", OrderJson);
copied = true;
StateHasChanged();
try { await Task.Delay(1500, _copyCts.Token); } catch { }
copied = false;
StateHasChanged();
}
// ================= DIALOG =================
async Task OpenEditNodeDialog(Node node)
{
var parameters = new DialogParameters<EditNodeDialog>
{
{ x => x.Node, node }
};
var options = new DialogOptions
{
CloseButton = true,
FullWidth = true,
MaxWidth = MaxWidth.Large
};
var dialog = await DialogService.ShowAsync<EditNodeDialog>(
$"Edit Node: {node.NodeId}", parameters, options);
await dialog.Result;
OnOrderChanged(); // 🔥 cập nhật JSON sau dialog
}
}

View File

@@ -0,0 +1,436 @@
@page "/robot-config"
@rendermode InteractiveWebAssemblyNoPrerender
@inject HttpClient Http
@inject ISnackbar Snackbar
@using System.Net.Http.Json
@using RobotApp.Common.Shares.Dtos
<PageTitle>Robot Configuration</PageTitle>
<div class="d-flex w-100 h-100 p-2 overflow-hidden flex-column position-relative">
<div class="rcm-toolbar">
<div class="rcm-toolbar-left">
<label class="rcm-label" for="configType">Config Type</label>
<div class="rcm-select-wrapper">
<select id="configType" class="form-select rcm-select" value="@SelectedType" @onchange="OnTypeChanged">
@foreach (var type in GetConfigType)
{
<option value="@type">@type</option>
}
</select>
<span class="rcm-select-icon" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M7 10l5 5 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</div>
</div>
<div class="rcm-toolbar-right">
<div class="rcm-action-group">
@* <button type="button" class="btn rcm-icon-btn" data-tooltip="Add config" aria-label="Add" @onclick="OpenAddConfig">
<i class="mdi mdi-plus" aria-hidden="true"></i>
</button> *@
<button type="button" class="btn rcm-icon-btn" data-tooltip="Update config" aria-label="Update" @onclick="SaveConfig">
<i class="mdi mdi-content-save" aria-hidden="true"></i>
</button>
<button type="button" class="btn rcm-icon-btn" data-tooltip="Load config" aria-label="Update" @onclick="LoadConfig">
<i class="mdi mdi-file-download" aria-hidden="true"></i>
</button>
@* <button type="button" class="btn rcm-icon-btn rcm-danger" data-tooltip="Delete config" aria-label="Delete" @onclick="DeleteConfig">
<i class="mdi mdi-delete" aria-hidden="true"></i>
</button> *@
</div>
</div>
</div>
<div class="content rcm-content flex-grow-1 d-flex gap-3 mt-3">
<div class="card p-2 config-list rcm-config-list">
<div class="card-body list-body p-0">
@switch (SelectedType)
{
case RobotConfigType.VDA5050:
@RenderList(VdaConfigs, SelectVda, v => v.ConfigName, v => v.IsActive)
break;
case RobotConfigType.Safety:
@RenderList(SafetyConfigs, SelectSafety, s => s.ConfigName, s => s.IsActive)
break;
case RobotConfigType.Simulation:
@RenderList(SimulationConfigs, SelectSimulation, s => s.ConfigName, s => s.IsActive)
break;
case RobotConfigType.PLC:
@RenderList(PlcConfigs, SelectPlc, p => p.ConfigName, p => p.IsActive)
break;
case RobotConfigType.Core:
@RenderList(CoreConfigs, SelectCore, c => c.ConfigName, c => c.IsActive)
break;
default:
<div class="p-2">No configs.</div>
break;
}
</div>
</div>
<div class="card p-2 config-content rcm-config-content">
<div class="card-body">
@if (!HasSelection)
{
<div class="text-muted">Select a config from the list or click Add to create one.</div>
}
else
{
@switch (SelectedType)
{
case RobotConfigType.VDA5050:
<RobotApp.Client.Pages.Components.Config.RobotVDA5050Config @ref="@RobotVDA5050ConfigRef" @bind-Model="SelectedVda" />
break;
case RobotConfigType.Safety:
<RobotApp.Client.Pages.Components.Config.RobotSafetyConfig @bind-Model="SelectedSafety" />
break;
case RobotConfigType.Simulation:
<RobotApp.Client.Pages.Components.Config.RobotSimulationConfig @bind-Model="SelectedSimulation" />
break;
case RobotConfigType.PLC:
<RobotApp.Client.Pages.Components.Config.RobotPLCConfig @bind-Model="SelectedPlc" />
break;
case RobotConfigType.Core:
<RobotApp.Client.Pages.Components.Config.RobotConfig @bind-Model="SelectedCore" />
break;
}
}
</div>
</div>
</div>
@if (IsLoading)
{
<div class="rcm-overlay" role="status" aria-live="polite" aria-busy="true">
<div class="rcm-overlay-content" aria-hidden="false">
<div class="spinner-border text-light" role="status" aria-hidden="true"></div>
<div class="rcm-overlay-message ms-2">Loading…</div>
</div>
</div>
}
@if (IsAddingNew)
{
<div class="rcm-modal-overlay" role="dialog" aria-modal="true">
<div class="rcm-modal">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Create @SelectedType Config</strong>
<button class="btn btn-sm btn-link text-muted" @onclick="CloseAddDialog" aria-label="Close">✕</button>
</div>
<div class="card-body">
<EditForm Model="addForm" OnValidSubmit="SaveNewConfigAsync">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-2">
<label class="form-label">Config name</label>
<InputText class="form-control" @bind-Value="addForm.ConfigName" />
</div>
<div class="mb-2">
<label class="form-label">Description</label>
<InputText class="form-control" @bind-Value="addForm.Description" />
</div>
<div class="mb-2">
<label class="form-label">Copy parameters from existing (@SelectedType)</label>
<select class="form-select" @bind="SelectedTemplateIdString">
@foreach (var t in GetTemplatesForSelectedType())
{
<option value="@t.Id">@t.Name</option>
}
</select>
<div class="form-text">If you choose an existing config, its parameter fields will be copied into the new config; you can still change name/description.</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-3">
<button type="button" class="btn btn-outline-secondary" @onclick="CloseAddDialog">Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</EditForm>
</div>
</div>
</div>
</div>
}
@if (ShowDeleteConfirm)
{
<div class="rcm-modal-overlay" role="dialog" aria-modal="true">
<div class="rcm-modal">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Confirm Delete</strong>
<button class="btn btn-sm btn-link text-muted" @onclick="CancelDelete" aria-label="Close">✕</button>
</div>
<div class="card-body">
<p>Are you sure you want to delete configuration "<strong>@DeletePendingName</strong>"?</p>
<div class="d-flex justify-content-end gap-2 mt-3">
<button class="btn btn-outline-secondary" @onclick="CancelDelete">Cancel</button>
<button class="btn btn-danger" @onclick="ConfirmDeleteAsync">Delete</button>
</div>
</div>
</div>
</div>
</div>
}
</div>
@code {
private List<RobotVDA5050ConfigDto> VdaConfigs { get; set; } = new();
private List<RobotSafetyConfigDto> SafetyConfigs { get; set; } = new();
private List<RobotSimulationConfigDto> SimulationConfigs { get; set; } = new();
private List<RobotPlcConfigDto> PlcConfigs { get; set; } = new();
private List<RobotConfigDto> CoreConfigs { get; set; } = new();
private RobotVDA5050ConfigDto? SelectedVda { get; set; }
private RobotSafetyConfigDto? SelectedSafety { get; set; }
private RobotSimulationConfigDto? SelectedSimulation { get; set; }
private RobotPlcConfigDto? SelectedPlc { get; set; }
private RobotConfigDto? SelectedCore { get; set; }
private CreateRobotVDA5050ConfigDto CreateVda = new();
private CreateRobotConfigDto CreateSafety = new();
private CreateRobotSafetyConfigDto CreateSimulation = new();
private CreateRobotSimulationConfigDto CreatePlc = new();
private CreateRobotPlcConfigDto CreateCore = new();
private RobotConfigType SelectedType = RobotConfigType.VDA5050;
private RobotApp.Client.Pages.Components.Config.RobotVDA5050Config RobotVDA5050ConfigRef = default!;
private int SelectedIndex = -1;
private bool HasSelection => SelectedIndex >= 0;
private bool IsLoading = false;
private bool IsAddingNew = false;
private bool ShowDeleteConfirm = false;
private Guid? DeletePendingId;
private string DeletePendingName = string.Empty;
private class AddFormModel
{
public string ConfigName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
private AddFormModel addForm = new();
private string SelectedTemplateIdString = string.Empty;
private IEnumerable<RobotConfigType> GetConfigType = [RobotConfigType.VDA5050, RobotConfigType.Simulation];
private IEnumerable<(Guid Id, string Name, bool Active)> GetTemplatesForSelectedType()
{
return SelectedType switch
{
RobotConfigType.VDA5050 => VdaConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
RobotConfigType.Safety => SafetyConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
RobotConfigType.Simulation => SimulationConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
RobotConfigType.PLC => PlcConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
RobotConfigType.Core => CoreConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
_ => [],
};
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!firstRender) return;
await LoadForTypeAsync(SelectedType);
}
private async Task OnTypeChanged(ChangeEventArgs e)
{
if (e?.Value is null) return;
if (!Enum.TryParse<RobotConfigType>(e.Value.ToString(), out var newType)) return;
if (newType == SelectedType) return;
SelectedType = newType;
await LoadForTypeAsync(newType);
}
private async Task LoadForTypeAsync(RobotConfigType type)
{
IsLoading = true;
StateHasChanged();
try
{
switch (type)
{
case RobotConfigType.VDA5050:
await LoadVDA5050Configs();
break;
case RobotConfigType.Safety:
await LoadRobotSafetyConfigs();
break;
case RobotConfigType.Simulation:
await LoadRobotSimulationConfigs();
break;
case RobotConfigType.PLC:
await LoadRobotPlcConfigs();
break;
case RobotConfigType.Core:
await LoadRobotConfigs();
break;
}
}
finally
{
IsLoading = false;
StateHasChanged();
}
}
RenderFragment RenderList<T>(List<T> list, Action<int> onSelect, Func<T, string> nameSelector, Func<T, bool>? isActiveSelector = null) where T : class
{
return builder =>
{
if (list is null || !list.Any())
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "p-2 text-muted");
builder.AddContent(2, "No configs found.");
builder.CloseElement();
return;
}
builder.OpenElement(3, "ul");
builder.AddAttribute(4, "class", "list-group list-group-flush");
for (int i = 0; i < list.Count; i++)
{
var item = list[i];
var idx = i;
builder.OpenElement(10 + i * 6, "li");
builder.AddAttribute(11 + i * 6, "class", $"list-group-item {(SelectedIndex == idx ? "active" : "")}");
builder.AddAttribute(12 + i * 6, "style", "cursor:pointer;padding:0.75rem 1rem;");
builder.AddAttribute(13 + i * 6, "onclick", EventCallback.Factory.Create(this, () => onSelect(idx)));
string name;
try
{
name = nameSelector(item) ?? "Unnamed";
}
catch
{
name = "Unnamed";
}
builder.AddContent(14 + i * 6, name);
bool isActive = false;
if (isActiveSelector is not null)
{
try
{
isActive = isActiveSelector(item);
}
catch
{
isActive = false;
}
}
if (isActive)
{
builder.OpenElement(15 + i * 6, "span");
builder.AddAttribute(16 + i * 6, "class", "badge bg-success ms-2 float-end");
builder.AddContent(17 + i * 6, "Active");
builder.CloseElement();
}
builder.CloseElement();
}
builder.CloseElement();
};
}
private Action<int> SelectVda => idx =>
{
SelectedIndex = idx;
SelectedVda = idx >= 0 && idx < VdaConfigs.Count ? VdaConfigs[idx] with { } : null;
StateHasChanged();
};
private Action<int> SelectSafety => idx =>
{
SelectedIndex = idx;
SelectedSafety = idx >= 0 && idx < SafetyConfigs.Count ? SafetyConfigs[idx] with { } : null;
StateHasChanged();
};
private Action<int> SelectSimulation => idx =>
{
SelectedIndex = idx;
SelectedSimulation = idx >= 0 && idx < SimulationConfigs.Count ? SimulationConfigs[idx] with { } : null;
StateHasChanged();
};
private Action<int> SelectPlc => idx =>
{
SelectedIndex = idx;
SelectedPlc = idx >= 0 && idx < PlcConfigs.Count ? PlcConfigs[idx] with { } : null;
StateHasChanged();
};
private Action<int> SelectCore => idx =>
{
SelectedIndex = idx;
SelectedCore = idx >= 0 && idx < CoreConfigs.Count ? CoreConfigs[idx] with { } : null;
StateHasChanged();
};
private void OpenAddConfig()
{
addForm = new AddFormModel();
var model = GetTemplatesForSelectedType();
var modelActive = model.Where(x => x.Active).ToList();
SelectedTemplateIdString = modelActive.Count > 0 ? modelActive.First().Id.ToString() : model.Any() ? model.First().Id.ToString() : string.Empty;
IsAddingNew = true;
}
private void CloseAddDialog()
{
IsAddingNew = false;
}
private void DeleteConfig()
{
var tuple = SelectedType switch
{
RobotConfigType.VDA5050 => (Id: SelectedVda?.Id, Name: SelectedVda?.ConfigName),
RobotConfigType.Safety => (Id: SelectedSafety?.Id, Name: SelectedSafety?.ConfigName),
RobotConfigType.Simulation => (Id: SelectedSimulation?.Id, Name: SelectedSimulation?.ConfigName),
RobotConfigType.PLC => (Id: SelectedPlc?.Id, Name: SelectedPlc?.ConfigName),
RobotConfigType.Core => (Id: SelectedCore?.Id, Name: SelectedCore?.ConfigName),
_ => (Id: (Guid?)null, Name: (string?)null)
};
if (tuple.Id is null || tuple.Id == Guid.Empty)
{
Snackbar.Add("No config selected to delete.", Severity.Warning);
return;
}
DeletePendingId = tuple.Id;
DeletePendingName = tuple.Name ?? string.Empty;
ShowDeleteConfirm = true;
}
private void CancelDelete()
{
ShowDeleteConfirm = false;
DeletePendingId = null;
DeletePendingName = string.Empty;
}
}

View File

@@ -0,0 +1,573 @@
using MudBlazor;
using RobotApp.Common.Shares;
using RobotApp.Common.Shares.Dtos;
using RobotApp.Common.Shares.Enums;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Xml.Schema;
namespace RobotApp.Client.Pages;
public partial class RobotConfigManager
{
private async Task LoadVDA5050Configs()
{
try
{
var res = await Http.GetFromJsonAsync<MessageResult<RobotVDA5050ConfigDto[]>>("api/RobotConfigs/vda5050");
if (res is null) Snackbar.Add("Failed to load VDA5050 configs", Severity.Warning);
else if (!res.IsSuccess) Snackbar.Add(res.Message ?? "Failed to load VDA5050 configs", Severity.Warning);
else if (res.Data is not null)
{
VdaConfigs.Clear();
VdaConfigs.AddRange(res.Data);
var activeIdx = VdaConfigs.FindIndex(x => x.IsActive);
if (activeIdx >= 0)
{
SelectedIndex = activeIdx;
SelectedVda = VdaConfigs[activeIdx] with { };
}
else
{
SelectedIndex = -1;
SelectedVda = null;
}
StateHasChanged();
}
else
{
Snackbar.Add("No VDA5050 configs found", Severity.Info);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error loading PLC configs: {ex.Message}", Severity.Warning);
}
}
private async Task LoadRobotConfigs()
{
try
{
var res = await Http.GetFromJsonAsync<MessageResult<RobotConfigDto[]>>("api/RobotConfigs/robot");
if (res is null) Snackbar.Add("Failed to load VDA5050 configs", Severity.Warning);
else if (!res.IsSuccess) Snackbar.Add(res.Message ?? "Failed to load VDA5050 configs", Severity.Warning);
else if (res.Data is not null)
{
CoreConfigs.Clear();
CoreConfigs.AddRange(res.Data);
var activeIdx = CoreConfigs.FindIndex(x => x.IsActive);
if (activeIdx >= 0)
{
SelectedIndex = activeIdx;
SelectedCore = CoreConfigs[activeIdx] with { };
}
else
{
SelectedIndex = -1;
SelectedCore = null;
}
StateHasChanged();
}
}
catch (Exception ex)
{
Snackbar.Add($"Error loading PLC configs: {ex.Message}", Severity.Warning);
}
}
private async Task LoadRobotSafetyConfigs()
{
try
{
var res = await Http.GetFromJsonAsync<MessageResult<RobotSafetyConfigDto[]>>("api/RobotConfigs/safety");
if (res is null) Snackbar.Add("Failed to load VDA5050 configs", Severity.Warning);
else if (!res.IsSuccess) Snackbar.Add(res.Message ?? "Failed to load VDA5050 configs", Severity.Warning);
else if (res.Data is not null)
{
SafetyConfigs.Clear();
SafetyConfigs.AddRange(res.Data);
var activeIdx = SafetyConfigs.FindIndex(x => x.IsActive);
if (activeIdx >= 0)
{
SelectedIndex = activeIdx;
SelectedSafety = SafetyConfigs[activeIdx] with { };
}
else
{
SelectedIndex = -1;
SelectedSafety = null;
}
StateHasChanged();
}
}
catch (Exception ex)
{
Snackbar.Add($"Error loading PLC configs: {ex.Message}", Severity.Warning);
}
}
private async Task LoadRobotSimulationConfigs()
{
try
{
var res = await Http.GetFromJsonAsync<MessageResult<RobotSimulationConfigDto[]>>("api/RobotConfigs/simulation");
if (res is null) Snackbar.Add("Failed to load VDA5050 configs", Severity.Warning);
else if (!res.IsSuccess) Snackbar.Add(res.Message ?? "Failed to load VDA5050 configs", Severity.Warning);
else if (res.Data is not null)
{
SimulationConfigs.Clear();
SimulationConfigs.AddRange(res.Data);
var activeIdx = SimulationConfigs.FindIndex(x => x.IsActive);
if (activeIdx >= 0)
{
SelectedIndex = activeIdx;
SelectedSimulation = SimulationConfigs[activeIdx] with { };
}
else
{
SelectedIndex = -1;
SelectedSimulation = null;
}
StateHasChanged();
}
}
catch (Exception ex)
{
Snackbar.Add($"Error loading PLC configs: {ex.Message}", Severity.Warning);
}
}
private async Task LoadRobotPlcConfigs()
{
try
{
var res = await Http.GetFromJsonAsync<MessageResult<RobotPlcConfigDto[]>>("api/RobotConfigs/plc");
if (res is null) Snackbar.Add("Failed to load VDA5050 configs", Severity.Warning);
else if (!res.IsSuccess) Snackbar.Add(res.Message ?? "Failed to load VDA5050 configs", Severity.Warning);
else if (res.Data is not null)
{
PlcConfigs.Clear();
PlcConfigs.AddRange(res.Data);
var activeIdx = PlcConfigs.FindIndex(x => x.IsActive);
if (activeIdx >= 0)
{
SelectedIndex = activeIdx;
SelectedPlc = PlcConfigs[activeIdx] with { };
}
else
{
SelectedIndex = -1;
SelectedPlc = null;
}
StateHasChanged();
}
}
catch (Exception ex)
{
Snackbar.Add($"Error loading PLC configs: {ex.Message}", Severity.Warning);
}
}
private async Task SaveNewConfigAsync()
{
try
{
HttpResponseMessage? res = null;
_ = Guid.TryParse(SelectedTemplateIdString, out Guid templateId);
switch (SelectedType)
{
case RobotConfigType.VDA5050:
{
var template = VdaConfigs.FirstOrDefault(x => x.Id == templateId);
if (template is null) return;
var payload = new
{
addForm.ConfigName,
addForm.Description,
template.SerialNumber,
template.VDA5050HostServer,
template.VDA5050Port,
template.VDA5050UserName,
template.VDA5050Password,
template.VDA5050Manufacturer,
template.VDA5050Version,
template.VDA5050TopicPrefix,
template.VDA5050PublishRepeat,
template.VDA5050EnablePassword,
template.VDA5050EnableTls
};
res = await Http.PostAsJsonAsync("api/RobotConfigs/vda5050", payload);
break;
}
case RobotConfigType.PLC:
{
var template = PlcConfigs.FirstOrDefault(x => x.Id == templateId);
if (template is null) return;
var payload = new
{
addForm.ConfigName,
addForm.Description,
template.PLCAddress,
template.PLCPort,
template.PLCUnitId
};
res = await Http.PostAsJsonAsync("api/RobotConfigs/plc", payload);
break;
}
case RobotConfigType.Safety:
{
var template = SafetyConfigs.FirstOrDefault(x => x.Id == templateId);
if (template is null) return;
var payload = new
{
addForm.ConfigName,
addForm.Description,
template.SafetySpeedVerySlow,
template.SafetySpeedSlow,
template.SafetySpeedNormal,
template.SafetySpeedMedium,
template.SafetySpeedOptimal,
template.SafetySpeedFast,
template.SafetySpeedVeryFast
};
res = await Http.PostAsJsonAsync("api/RobotConfigs/safety", payload);
break;
}
case RobotConfigType.Simulation:
{
var template = SimulationConfigs.FirstOrDefault(x => x.Id == templateId);
if (template is null) return;
var payload = new
{
addForm.ConfigName,
addForm.Description,
template.EnableSimulation,
template.SimulationMaxVelocity,
template.SimulationMaxAngularVelocity,
template.SimulationAcceleration,
template.SimulationDeceleration
};
res = await Http.PostAsJsonAsync("api/RobotConfigs/simulation", payload);
break;
}
case RobotConfigType.Core:
default:
{
var template = CoreConfigs.FirstOrDefault(x => x.Id == templateId);
if (template is null) return;
var payload = new
{
addForm.ConfigName,
addForm.Description,
template.NavigationType,
template.RadiusWheel,
template.Width,
template.Length,
template.Height
};
res = await Http.PostAsJsonAsync("api/RobotConfigs/robot", payload);
break;
}
}
if (res is not null && res.IsSuccessStatusCode)
{
Snackbar.Add("Config created", Severity.Success);
IsAddingNew = false;
await LoadForTypeAsync(SelectedType);
}
else
{
var message = res is null ? "No response" : await res.Content.ReadAsStringAsync();
Snackbar.Add($"Create failed: {message}", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error creating config: {ex.Message}", Severity.Error);
}
}
private async Task<bool> SaveCertificates()
{
using var content = new MultipartFormDataContent();
if (RobotVDA5050ConfigRef.CaFile is not null)
{
var fileContent = new StreamContent(RobotVDA5050ConfigRef.CaFile.OpenReadStream(maxAllowedSize: RobotVDA5050ConfigRef.MaxFileSize));
content.Add(fileContent, "CaFile", RobotVDA5050ConfigRef.CaFile.Name);
}
if (RobotVDA5050ConfigRef.CertFile is not null)
{
var fileContent = new StreamContent(RobotVDA5050ConfigRef.CertFile.OpenReadStream(maxAllowedSize: RobotVDA5050ConfigRef.MaxFileSize));
content.Add(fileContent, "CertFile", RobotVDA5050ConfigRef.CertFile.Name);
}
if (RobotVDA5050ConfigRef.KeyFile is not null)
{
var fileContent = new StreamContent(RobotVDA5050ConfigRef.KeyFile.OpenReadStream(maxAllowedSize: RobotVDA5050ConfigRef.MaxFileSize));
content.Add(fileContent, "KeyFile", RobotVDA5050ConfigRef.KeyFile.Name);
}
if (content.Any())
{
var response = await (await Http.PostAsync($"api/File/certificates", content)).Content.ReadFromJsonAsync<MessageResult>();
if (response is null) Snackbar.Add("Failed to update certificates", Severity.Warning);
else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Failed to update certificates", Severity.Warning);
else return true;
StateHasChanged();
return false;
}
return true;
}
private async Task SaveConfig()
{
try
{
Guid? id = SelectedType switch
{
RobotConfigType.VDA5050 => SelectedVda?.Id,
RobotConfigType.Safety => SelectedSafety?.Id,
RobotConfigType.Simulation => SelectedSimulation?.Id,
RobotConfigType.PLC => SelectedPlc?.Id,
RobotConfigType.Core => SelectedCore?.Id,
_ => null
};
if (id == null || id == Guid.Empty)
{
Snackbar.Add("No config selected to save.", Severity.Warning);
return;
}
MessageResult? result = null;
switch (SelectedType)
{
case RobotConfigType.VDA5050:
{
if (SelectedVda is null) { Snackbar.Add("No VDA5050 config selected.", Severity.Warning); return; }
var updateDto = new
{
SelectedVda.SerialNumber,
SelectedVda.VDA5050HostServer,
SelectedVda.VDA5050Port,
SelectedVda.VDA5050UserName,
SelectedVda.VDA5050Password,
SelectedVda.VDA5050Manufacturer,
SelectedVda.VDA5050Version,
SelectedVda.VDA5050TopicPrefix,
SelectedVda.VDA5050PublishRepeat,
SelectedVda.VDA5050EnablePassword,
SelectedVda.VDA5050EnableTls,
SelectedVda.VDA5050CA,
SelectedVda.VDA5050Cer,
SelectedVda.VDA5050Key,
SelectedVda.Description
};
var saveCer = await SaveCertificates();
if (saveCer) result = await (await Http.PutAsJsonAsync($"api/RobotConfigs/vda5050/{id}", updateDto)).Content.ReadFromJsonAsync<MessageResult>();
else return;
break;
}
case RobotConfigType.PLC:
{
if (SelectedPlc is null) { Snackbar.Add("No PLC config selected.", Severity.Warning); return; }
var updateDto = new
{
SelectedPlc.Description,
SelectedPlc.PLCAddress,
SelectedPlc.PLCPort,
SelectedPlc.PLCUnitId
};
result = await (await Http.PutAsJsonAsync($"api/RobotConfigs/plc/{id}", updateDto)).Content.ReadFromJsonAsync<MessageResult>();
break;
}
case RobotConfigType.Safety:
{
if (SelectedSafety is null) { Snackbar.Add("No Safety config selected.", Severity.Warning); return; }
var updateDto = new
{
SelectedSafety.SafetySpeedVerySlow,
SelectedSafety.SafetySpeedSlow,
SelectedSafety.SafetySpeedNormal,
SelectedSafety.SafetySpeedMedium,
SelectedSafety.SafetySpeedOptimal,
SelectedSafety.SafetySpeedFast,
SelectedSafety.SafetySpeedVeryFast,
SelectedSafety.Description
};
result = await (await Http.PutAsJsonAsync($"api/RobotConfigs/safety/{id}", updateDto)).Content.ReadFromJsonAsync<MessageResult>();
break;
}
case RobotConfigType.Simulation:
{
if (SelectedSimulation is null) { Snackbar.Add("No Simulation config selected.", Severity.Warning); return; }
var updateDto = new
{
SelectedSimulation.EnableSimulation,
SelectedSimulation.SimulationMaxVelocity,
SelectedSimulation.SimulationMaxAngularVelocity,
SelectedSimulation.SimulationAcceleration,
SelectedSimulation.SimulationDeceleration,
SelectedSimulation.Description
};
result = await (await Http.PutAsJsonAsync($"api/RobotConfigs/simulation/{id}", updateDto)).Content.ReadFromJsonAsync<MessageResult>();
break;
}
case RobotConfigType.Core:
default:
{
if (SelectedCore is null) { Snackbar.Add("No Core config selected.", Severity.Warning); return; }
var updateDto = new
{
SelectedCore.NavigationType,
SelectedCore.RadiusWheel,
SelectedCore.Width,
SelectedCore.Length,
SelectedCore.Height,
SelectedCore.Description
};
result = await (await Http.PutAsJsonAsync($"api/RobotConfigs/robot/{id}", updateDto)).Content.ReadFromJsonAsync<MessageResult>();
break;
}
}
if (result is null) Snackbar.Add("Failed to update config", Severity.Warning);
else if (!result.IsSuccess) Snackbar.Add(result.Message ?? "Failed to update config", Severity.Warning);
else
{
Snackbar.Add("Config saved", Severity.Success);
await LoadForTypeAsync(SelectedType);
switch (SelectedType)
{
case RobotConfigType.VDA5050:
var vIdx = VdaConfigs.FindIndex(x => x.Id == id);
if (vIdx >= 0) { SelectedIndex = vIdx; SelectedVda = VdaConfigs[vIdx] with { }; }
break;
case RobotConfigType.Safety:
var sIdx = SafetyConfigs.FindIndex(x => x.Id == id);
if (sIdx >= 0) { SelectedIndex = sIdx; SelectedSafety = SafetyConfigs[sIdx] with { }; }
break;
case RobotConfigType.Simulation:
var simIdx = SimulationConfigs.FindIndex(x => x.Id == id);
if (simIdx >= 0) { SelectedIndex = simIdx; SelectedSimulation = SimulationConfigs[simIdx] with { }; }
break;
case RobotConfigType.PLC:
var pIdx = PlcConfigs.FindIndex(x => x.Id == id);
if (pIdx >= 0) { SelectedIndex = pIdx; SelectedPlc = PlcConfigs[pIdx] with { }; }
break;
case RobotConfigType.Core:
var cIdx = CoreConfigs.FindIndex(x => x.Id == id);
if (cIdx >= 0) { SelectedIndex = cIdx; SelectedCore = CoreConfigs[cIdx] with { }; }
break;
}
StateHasChanged();
}
}
catch (Exception ex)
{
Snackbar.Add($"Error saving config: {ex.Message}", Severity.Error);
}
}
private async Task ConfirmDeleteAsync()
{
try
{
if (DeletePendingId is null || DeletePendingId == Guid.Empty)
{
Snackbar.Add("No config selected to delete.", Severity.Warning);
CancelDelete();
return;
}
var id = DeletePendingId.Value;
string path = SelectedType switch
{
RobotConfigType.VDA5050 => $"api/RobotConfigs/vda5050/{id}",
RobotConfigType.Safety => $"api/RobotConfigs/safety/{id}",
RobotConfigType.Simulation => $"api/RobotConfigs/simulation/{id}",
RobotConfigType.PLC => $"api/RobotConfigs/plc/{id}",
RobotConfigType.Core => $"api/RobotConfigs/robot/{id}",
_ => throw new InvalidOperationException("Unsupported config type")
};
IsLoading = true;
StateHasChanged();
var httpRes = await Http.DeleteFromJsonAsync<MessageResult>(path);
if (httpRes is null) Snackbar.Add("Failed to delete config", Severity.Warning);
else if (!httpRes.IsSuccess) Snackbar.Add(httpRes.Message ?? "Failed to delete config", Severity.Warning);
else
{
Snackbar.Add("Config deleted", Severity.Success);
SelectedIndex = -1;
switch (SelectedType)
{
case RobotConfigType.VDA5050: SelectedVda = null; break;
case RobotConfigType.Safety: SelectedSafety = null; break;
case RobotConfigType.Simulation: SelectedSimulation = null; break;
case RobotConfigType.PLC: SelectedPlc = null; break;
case RobotConfigType.Core: SelectedCore = null; break;
}
await LoadForTypeAsync(SelectedType);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error deleting config: {ex.Message}", Severity.Error);
}
finally
{
IsLoading = false;
CancelDelete();
StateHasChanged();
}
}
private async Task LoadConfig()
{
IsLoading = true;
StateHasChanged();
var response = await (await Http.PostAsync($"api/RobotConfigs/load", null)).Content.ReadFromJsonAsync<MessageResult>();
if (response is null) Snackbar.Add("Failed to load config", Severity.Warning);
else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Failed to load config", Severity.Warning);
else Snackbar.Add("Config loaded", Severity.Success);
IsLoading = false;
StateHasChanged();
}
}

View File

@@ -0,0 +1,520 @@
/* Toolbar layout */
.rcm-toolbar {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
background: var(--mud-palette-surface, #ffffff);
color: #e6e6e6;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.6);
border: 1px solid rgba(255,255,255,0.04);
}
/* Content layout: take remaining height */
.rcm-content {
height: calc(100vh - 90px); /* approximate toolbar + padding height; adjust if needed */
min-height: 0; /* allow flex children to shrink properly */
}
/* Config list uses flexible basis and can shrink instead of forcing overflow */
.rcm-config-list {
/* replace fixed width with flex basis + shrink */
flex: 0 1 35%; /* flex-grow:0, flex-shrink:1, flex-basis:35% */
min-width: 240px; /* allow smaller than before but keep readable */
display: flex;
flex-direction: column;
min-height: 0;
}
/* Config content uses remaining space and can shrink/grow */
.rcm-config-content {
flex: 1 1 65%; /* flex-grow:1 to take remaining, flex-shrink:1 */
min-width: 220px;
display: flex;
flex-direction: column;
}
/* Make card body scrollable if content overflows */
.config-list .list-body,
.rcm-config-content .card-body,
.config-list .list-body ul {
overflow: auto;
}
/* Ensure list-body and editor body expand to fill container */
.config-list .list-body {
height: 100%;
}
.rcm-config-content .card-body {
height: 100%;
}
/* Make config list body fill available height and scroll when content overflows */
.rcm-config-list .card-body.list-body {
flex: 1 1 auto; /* expand to fill container */
min-height: 0; /* allow flex children to shrink in many browsers */
overflow: auto; /* enable scrolling when content overflows */
padding: 0.25rem 0.5rem; /* keep slight padding for list */
}
/* Ensure the list itself doesn't add extra margins that affect scrolling */
.rcm-config-list .list-group {
margin: 0;
padding: 0;
}
/* Each list item keeps its padding but stays in flow */
.rcm-config-list .list-group-item {
padding: 0.75rem 1rem;
}
/* List items spacing */
.list-group-item {
cursor: pointer;
}
/* Left side controls (robot id + select) */
.rcm-toolbar-left {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
/* Right side action buttons */
.rcm-toolbar-right {
display: flex;
align-items: center;
}
/* Group buttons with small spacing */
.rcm-action-group {
display: flex;
gap: 0.5rem;
}
/* Inputs sizing */
.rcm-input {
width: 160px;
background: #1a1a1a;
border: 1px solid rgba(255,255,255,0.06);
color: #e6e6e6;
}
.rcm-select-wrapper {
position: relative;
display: inline-flex;
align-items: center;
}
/* Make select background brighter and match button gradients (blue tone similar to Update) */
.rcm-select {
width: 180px;
margin-left: 6px; /* bring select closer to left controls */
background: linear-gradient(180deg, #4aa0db, #2b87c9);
border: 1px solid rgba(0,0,0,0.12);
color: #fff;
appearance: none;
padding-right: 28px; /* space for icon */
box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset;
}
/* Hover effect: slightly brighter */
.rcm-select-wrapper:hover .rcm-select {
background: linear-gradient(180deg, #5bb6ee, #399ad6);
border-color: rgba(58,123,184,0.22);
transform: translateY(-1px);
}
/* Select icon color to contrast with brighter select */
.rcm-select-icon {
position: absolute;
right: 6px;
pointer-events: none;
color: rgba(255,255,255,0.9);
display: inline-flex;
align-items: center;
}
/* Toolbar labels */
.rcm-label {
font-size: 0.9rem;
margin-right: 6px;
color: #cfcfcf;
}
/* Minor button styling to match toolbar */
.rcm-btn {
min-width: 84px;
}
/* Icon button styles */
.rcm-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 34px;
padding: 0.25rem;
border-radius: 6px;
background: transparent;
color: #e6e6e6;
border: 1px solid rgba(255,255,255,0.04);
}
.rcm-icon-btn svg {
display: block;
}
/* Icon button hover and focus states */
.rcm-icon-btn:hover, .rcm-icon-btn:focus {
background: rgba(255,255,255,0.03);
color: #fff;
border-color: rgba(255,255,255,0.08);
}
.rcm-danger {
color: #ffb3b3;
border-color: rgba(255,100,100,0.18);
}
/* Adjust bootstrap button colors for dark toolbar when using outline variants */
.rcm-toolbar .btn-outline-primary {
color: #cfe2ff;
border-color: rgba(95,160,255,0.2);
}
.rcm-toolbar .btn-outline-success {
color: #d4f5d4;
border-color: rgba(60,200,120,0.18);
}
.rcm-toolbar .btn-outline-danger {
color: #ffcfcf;
border-color: rgba(255,100,100,0.18);
}
/* Specific styles for action buttons (Add, Update, Delete) */
.rcm-action-group button[aria-label="Add"] {
background: linear-gradient(180deg, #4bb24b, #2f9a2f); /* bright green */
color: #fff;
border: 1px solid rgba(0,0,0,0.08);
box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset;
}
.rcm-action-group button[aria-label="Add"]:hover {
background: linear-gradient(180deg, #66d166, #3fb83f);
transform: translateY(-1px);
}
.rcm-action-group button[aria-label="Add"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(56,160,56,0.18);
}
.rcm-action-group button[aria-label="Update"] {
background: linear-gradient(180deg, #4aa0db, #2b87c9); /* bright blue */
color: #fff;
border: 1px solid rgba(0,0,0,0.08);
box-shadow: 0 1px 0 rgba(255,255,255,0.08) inset;
}
.rcm-action-group button[aria-label="Update"]:hover {
background: linear-gradient(180deg, #5bb6ee, #399ad6);
transform: translateY(-1px);
}
.rcm-action-group button[aria-label="Update"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(58,123,184,0.18);
}
.rcm-action-group button[aria-label="Delete"] {
background: linear-gradient(180deg, #ff6b6b, #e04848); /* bright red */
color: #fff;
border: 1px solid rgba(0,0,0,0.08);
box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset;
}
.rcm-action-group button[aria-label="Delete"]:hover {
background: linear-gradient(180deg, #ff8282, #ec5b5b);
transform: translateY(-1px);
}
.rcm-action-group button[aria-label="Delete"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(224,72,72,0.18);
}
/* Ensure icons inside the action buttons remain visible */
.rcm-action-group button[aria-label="Add"] .mdi,
.rcm-action-group button[aria-label="Update"] .mdi,
.rcm-action-group button[aria-label="Delete"] .mdi {
color: #fff;
}
/* Ensure dropdown options use dark theme matching select */
.rcm-select option {
background-color: #141414 !important;
color: #e6e6e6 !important;
}
/* Hover/active state inside dropdown */
.rcm-select option:hover,
.rcm-select option:checked {
background-color: #2b87c9 !important; /* match update button blue */
color: #fff !important;
}
/* Optgroup styling (if used) */
.rcm-select optgroup {
color: #e6e6e6;
background: #141414;
}
/* Tooltip for action buttons using data-tooltip attribute */
.rcm-action-group button[data-tooltip] {
position: relative;
}
.rcm-action-group button[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
transform: translateX(-50%) translateY(8px);
bottom: -9999px; /* hidden by default below flow */
background: rgba(20,20,20,0.98);
color: #fff;
padding: 6px 10px;
border-radius: 6px;
font-size: 0.85rem;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(0,0,0,0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.12s ease, transform 0.12s ease;
z-index: 50;
}
/* Arrow below tooltip */
.rcm-action-group button[data-tooltip]::before {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -9999px;
width: 8px;
height: 8px;
background: rgba(20,20,20,0.98);
transform-origin: center;
rotate: 45deg;
z-index: 49;
}
/* Show tooltip on hover */
.rcm-action-group button[data-tooltip]:hover::after,
.rcm-action-group button[data-tooltip]:hover::before {
bottom: -45px; /* reduced gap: place tooltip closer to the button */
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Adjust for danger button (slightly different color) */
.rcm-action-group button[data-tooltip].rcm-danger::after {
background: rgba(224,72,72,0.95);
}
/* Slightly lift the button on hover to match tooltip */
.rcm-action-group button[data-tooltip]:hover {
transform: translateY(-2px);
}
/* Tooltip backgrounds matching their buttons */
.rcm-action-group button[aria-label="Add"][data-tooltip]::after {
background: linear-gradient(180deg, #4bb24b, #2f9a2f);
}
.rcm-action-group button[aria-label="Add"][data-tooltip]::before {
background: #2f9a2f;
}
.rcm-action-group button[aria-label="Update"][data-tooltip]::after {
background: linear-gradient(180deg, #4aa0db, #2b87c9);
}
.rcm-action-group button[aria-label="Update"][data-tooltip]::before {
background: #2b87c9;
}
/* Delete already had a red variant, update the arrow to match precisely */
.rcm-action-group button[aria-label="Delete"][data-tooltip]::after,
.rcm-action-group button[data-tooltip].rcm-danger::after {
background: linear-gradient(180deg, #ff6b6b, #e04848);
}
.rcm-action-group button[aria-label="Delete"][data-tooltip]::before,
.rcm-action-group button[data-tooltip].rcm-danger::before {
background: #e04848;
}
/* Ensure tooltip text stays readable on gradients */
.rcm-action-group button[data-tooltip]::after {
color: #fff;
}
/* Active badge in config list: make slightly larger and more visible */
.rcm-config-list .list-group-item .badge {
font-size: 1.05rem; /* slightly larger */
padding: 0.45rem 0.8rem;
border-radius: 0.5rem;
line-height: 1;
margin-left: 0.6rem;
opacity: 0.98;
display: inline-block;
vertical-align: middle;
}
/* Overlay that blocks the whole viewport while loading */
.rcm-overlay {
position: absolute;
inset: 0; /* top:0; right:0; bottom:0; left:0; */
display: flex;
align-items: center;
justify-content: center;
background: rgba(7, 10, 13, 0.55);
z-index: 1050; /* above most UI layers */
pointer-events: auto; /* capture mouse interactions */
}
/* inner content: spinner + optional message */
.rcm-overlay-content {
display: inline-flex;
align-items: center;
gap: 0.6rem;
background: rgba(20, 20, 20, 0.85);
padding: 0.75rem 1rem;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
color: #fff;
font-weight: 500;
transform: translateY(-6px);
}
/* message text */
.rcm-overlay-message {
font-size: 0.95rem;
color: #eef3ff;
}
/* ensure spinner is visible on dark overlay */
.rcm-overlay .spinner-border {
width: 1.25rem;
height: 1.25rem;
border-width: 0.18rem;
color: #ffffff;
}
/* Modal overlay (create dialog) */
.rcm-modal-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(7, 10, 13, 0.45);
z-index: 1065;
pointer-events: auto;
}
/* Modal container */
.rcm-modal {
width: 820px;
max-width: calc(100% - 40px);
border-radius: 8px;
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
background: var(--mud-palette-surface, #0f1720);
color: #e6e6e6;
}
/* Ensure card inside modal uses default spacing already present */
.rcm-modal .card {
background: #0f1720;
border: 1px solid rgba(255,255,255,0.04);
}
/* Header styling */
.rcm-modal .card-header {
padding: 0.6rem 0.9rem;
background: rgba(255,255,255,0.02);
border-bottom: 1px solid rgba(255,255,255,0.03);
}
/* Modal message / spinner adjustments (already defined for overlay) */
.rcm-overlay .spinner-border {
width: 1.25rem;
height: 1.25rem;
border-width: 0.18rem;
color: #ffffff;
}
/* Keep responsive layout for modal form inputs */
.rcm-modal .card-body {
padding: 0.8rem;
color: #e6e6e6;
}
/* --- Contrast adjustments for modal form elements --- */
/* Improve readability by increasing text contrast and adjusting input backgrounds */
.rcm-modal {
color: #eaf4ff; /* default text inside modal */
}
/* labels, headers and small notes */
.rcm-modal .form-label,
.rcm-modal label,
.rcm-modal .card-header strong,
.rcm-modal .card-header,
.rcm-modal .form-text,
.rcm-modal .validation-message,
.rcm-modal .text-muted {
color: #cfe6ff;
}
/* form controls */
.rcm-modal .form-control,
.rcm-modal .form-select,
.rcm-modal input[type="text"],
.rcm-modal input[type="number"],
.rcm-modal textarea {
/*background: rgba(255,255,255,0.03);*/ /* subtle contrast with modal bg */
/*color: #ffffff;*/
border: 1px solid rgba(255,255,255,0.08);
box-shadow: none;
caret-color: #ffffff;
}
/* placeholder text */
.rcm-modal .form-control::placeholder,
.rcm-modal input::placeholder,
.rcm-modal textarea::placeholder {
color: rgba(255,255,255,0.55);
}
/* validation / summary messages */
.rcm-modal .validation-message,
.rcm-modal .validation-summary-valid,
.rcm-modal .validation-summary-errors,
.rcm-modal .field-validation-error {
color: #ffd2d2;
}
/* modal buttons */
.rcm-modal .btn-link {
color: #cfcfcf;
}
/* keep modal small text readable */
.rcm-modal .card-body {
font-size: 0.95rem;
line-height: 1.4;
}

View File

@@ -0,0 +1,51 @@
@page "/robot-monitor"
@rendermode InteractiveWebAssemblyNoPrerender
@inject RobotApp.Client.Services.RobotMonitorService MonitorService
@implements IAsyncDisposable
<PageTitle>Robot Monitor</PageTitle>
<div class="d-flex w-100 h-100 overflow-hidden">
<RobotApp.Client.Pages.Components.Monitor.RobotMonitorView @ref="@RobotMonitorViewRef"
MonitorData="@_monitorData"
IsConnected="@MonitorService.IsConnected" />
</div>
@code {
private RobotMonitorDto? _monitorData;
private RobotApp.Client.Pages.Components.Monitor.RobotMonitorView? RobotMonitorViewRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
MonitorService.OnDataReceived += OnMonitorDataReceived;
await MonitorService.StartAsync();
}
}
private void OnMonitorDataReceived(RobotMonitorDto data)
{
_monitorData = data;
RobotMonitorViewRef?.UpdatePath();
RobotMonitorViewRef?.OnMonitorDataUpdated();
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
MonitorService.OnDataReceived -= OnMonitorDataReceived;
await MonitorService.StopAsync();
}
}

View File

@@ -1,63 +0,0 @@
@page "/weather"
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate a loading indicator
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,26 @@
@layout RobotApp.Client.Pages.AssemblyLayout
@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 static RobotApp.Client.ClientRenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using RobotApp.Client
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
@using RobotApp.Common.Shares
@using RobotApp.Common.Shares.Dtos
@using Excubo.Blazor.Canvas
@using Excubo.Blazor.Canvas.Contexts
@using System.Text.Json
@using System.Text.Json.Serialization
@using RobotApp.Client.Pages.Order
@using RobotApp.Client.Services
@using RobotApp.VDA5050.InstantAction
@using RobotApp.VDA5050.Order
@using RobotApp.VDA5050.Type
@using System.ComponentModel.DataAnnotations

View File

@@ -1,11 +1,24 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MudBlazor.Services; using MudBlazor.Services;
using RobotApp.Client.Services;
using System.Globalization;
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddAuthorizationCore(); builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState(); builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization(); builder.Services.AddAuthenticationStateDeserialization();
builder.Services.AddMudServices();
builder.Services.AddScoped(_ => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<RobotApp.Client.Services.RobotMonitorService>();
builder.Services.AddScoped<RobotApp.Client.Services.RobotStateClient>();
builder.Services.AddMudServices(config =>
{
config.SnackbarConfiguration.VisibleStateDuration = 2000;
config.SnackbarConfiguration.HideTransitionDuration = 500;
config.SnackbarConfiguration.ShowTransitionDuration = 500;
});
await builder.Build().RunAsync(); await builder.Build().RunAsync();

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile> <NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
@@ -9,9 +9,17 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.9" /> <PackageReference Include="Excubo.Blazor.Canvas" Version="3.2.91" />
<PackageReference Include="MudBlazor" Version="8.12.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
<PackageReference Include="MudBlazor" Version="8.15.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RobotApp.Common.Shares\RobotApp.Common.Shares.csproj" />
<ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using RobotApp.Common.Shares.Dtos;
namespace RobotApp.Client.Services;
public class RobotMonitorService : IAsyncDisposable
{
private HubConnection? _hubConnection;
private readonly string _hubUrl;
public event Action<RobotMonitorDto>? OnDataReceived;
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
public RobotMonitorService(NavigationManager navigationManager)
{
var baseUrl = navigationManager.BaseUri.TrimEnd('/');
_hubUrl = $"{baseUrl}/hubs/robotMonitor";
}
public async Task StartAsync()
{
if (_hubConnection is not null) return;
_hubConnection = new HubConnectionBuilder()
.WithUrl(_hubUrl)
.WithAutomaticReconnect()
.Build();
_hubConnection.On<RobotMonitorDto>("ReceiveRobotMonitorData", data =>
{
OnDataReceived?.Invoke(data);
});
await _hubConnection.StartAsync();
}
public async Task StopAsync()
{
if (_hubConnection is null) return;
await _hubConnection.StopAsync();
await _hubConnection.DisposeAsync();
_hubConnection = null;
}
public async ValueTask DisposeAsync()
{
await StopAsync();
}
}

View File

@@ -0,0 +1,197 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using RobotApp.Common.Shares;
using RobotApp.VDA5050.State;
using System.Collections.Concurrent;
using System.Text.Json;
namespace RobotApp.Client.Services;
// ================= SIGNALR CONNECTION STATE =================
public enum RobotClientState
{
Disconnected,
Connecting,
Connected,
Reconnecting
}
// ================= ROBOT STATE CLIENT =================
public sealed class RobotStateClient : IAsyncDisposable
{
private readonly NavigationManager _nav;
private HubConnection? _connection;
private readonly object _lock = new();
private bool _started;
// ================= STATE CACHE =================
public ConcurrentDictionary<string, StateMsg> LatestStates { get; } = new();
// ================= ROBOT CONNECTION =================
private bool _isRobotConnected;
public bool IsRobotConnected => _isRobotConnected;
// ================= EVENTS =================
public event Action<string, StateMsg>? OnStateReceived;
public event Action<StateMsg>? OnStateReceivedAny;
public event Action<bool>? OnRobotConnectionChanged;
public event Action<RobotClientState>? OnConnectionStateChanged;
public RobotClientState ConnectionState { get; private set; } = RobotClientState.Disconnected;
// ================= CTOR =================
public RobotStateClient(NavigationManager nav)
{
_nav = nav;
}
// ================= STATE HELPER =================
private void SetState(RobotClientState state)
{
if (ConnectionState == state)
return;
ConnectionState = state;
OnConnectionStateChanged?.Invoke(state);
}
// ================= START =================
public async Task StartAsync(string hubPath = "/hubs/robot")
{
lock (_lock)
{
if (_started) return;
_started = true;
}
SetState(RobotClientState.Connecting);
_connection = new HubConnectionBuilder()
.WithUrl(_nav.ToAbsoluteUri(hubPath))
.WithAutomaticReconnect(new[]
{
TimeSpan.Zero,
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(10)
})
.Build();
_connection.Reconnecting += _ =>
{
SetState(RobotClientState.Reconnecting);
return Task.CompletedTask;
};
_connection.Reconnected += _ =>
{
SetState(RobotClientState.Connected);
return Task.CompletedTask;
};
_connection.Closed += async _ =>
{
_started = false;
SetState(RobotClientState.Disconnected);
if (_connection != null)
{
try
{
await StartAsync(hubPath);
}
catch { }
}
};
// ================= SIGNALR HANDLERS =================
// VDA5050 State
_connection.On<StateMsg>("ReceiveState", HandleState);
// Robot connection (bool only)
_connection.On<bool>("ReceiveRobotConnection", HandleRobotConnection);
try
{
await _connection.StartAsync();
SetState(RobotClientState.Connected);
}
catch
{
_started = false;
SetState(RobotClientState.Disconnected);
}
}
// ================= HANDLE STATE =================
private void HandleState(StateMsg state)
{
if (state?.SerialNumber == null)
return;
LatestStates[state.SerialNumber] = state;
OnStateReceived?.Invoke(state.SerialNumber, state);
OnStateReceivedAny?.Invoke(state);
}
// ================= HANDLE ROBOT CONNECTION =================
private void HandleRobotConnection(bool isConnected)
{
_isRobotConnected = isConnected;
OnRobotConnectionChanged?.Invoke(isConnected);
}
// ================= SUBSCRIBE =================
public async Task SubscribeRobotAsync(string serialNumber)
{
if (_connection?.State != HubConnectionState.Connected)
return;
try
{
await _connection.InvokeAsync("JoinRobot", serialNumber);
}
catch
{
// ignore reconnect sẽ tự join lại
}
}
public async Task UnsubscribeRobotAsync(string serialNumber)
{
if (_connection?.State == HubConnectionState.Connected)
{
try
{
await _connection.InvokeAsync("LeaveRobot", serialNumber);
}
catch { }
}
LatestStates.TryRemove(serialNumber, out _);
_isRobotConnected = false;
}
// ================= GET CACHE =================
public StateMsg? GetLatestState()
{
if (!LatestStates.IsEmpty) return LatestStates.First().Value;
return null;
}
// ================= DISPOSE =================
public async ValueTask DisposeAsync()
{
_started = false;
_isRobotConnected = false;
SetState(RobotClientState.Disconnected);
if (_connection != null)
{
await _connection.DisposeAsync();
_connection = null;
}
}
}

View File

@@ -0,0 +1,74 @@
using RobotApp.VDA5050.InstantAction;
using RobotApp.VDA5050.Order;
using System.Text.Json.Serialization;
namespace RobotApp.Client.Services;
// ======================================================
// ORDER MESSAGE
// ======================================================
public class OrderMessage
{
public uint HeaderId { get; set; }
public string Timestamp { get; set; } = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
public string Version { get; set; } = "2.1.0";
public string Manufacturer { get; set; } = "PhenikaaX";
public string SerialNumber { get; set; } = "T800-003";
public string OrderId { get; set; } = "";
public int OrderUpdateId { get; set; }
public string? ZoneSetId { get; set; }
public List<Node> Nodes { get; set; } = [];
public List<Edge> Edges { get; set; } = [];
public OrderMsg ToSchemaObject()
{
return new OrderMsg
{
HeaderId = (uint)HeaderId++,
Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
Version = Version,
Manufacturer = Manufacturer,
SerialNumber = SerialNumber,
OrderId = OrderId= Guid.NewGuid().ToString(),
OrderUpdateId = OrderUpdateId,
ZoneSetId = string.IsNullOrWhiteSpace(ZoneSetId)
? null
: ZoneSetId,
Nodes = [..Nodes],
Edges = [..Edges],
};
}
public void Import(OrderMsg order)
{
HeaderId = order.HeaderId;
Timestamp = order.Timestamp;
Version = order.Version;
Manufacturer = order.Manufacturer;
SerialNumber = order.SerialNumber;
OrderId = order.OrderId;
ZoneSetId = order.ZoneSetId;
OrderUpdateId = order.OrderUpdateId;
Nodes = [.. order.Nodes];
Edges = [.. order.Edges];
}
}
// ======================================================
// UI ACTION PARAM
// ======================================================
public class UiActionParameter : ActionParameter
{
[JsonIgnore]
public string ValueString
{
get => Value?.ToString() ?? "";
set => Value = value;
}
}

View File

@@ -5,7 +5,14 @@
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using static RobotApp.Client.ClientRenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using RobotApp.Client @using RobotApp.Client
@using Microsoft.AspNetCore.Authorization
@using MudBlazor @using MudBlazor
@using RobotApp.Common.Shares.Dtos
@using RobotApp.Common.Shares.Enums
@using Blazored.LocalStorage
@using RobotApp.Client.Services
@using RobotApp.VDA5050.State

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,10 @@
window.copyToClipboardFallback = function (text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}

View File

@@ -0,0 +1,107 @@
window.getElementSize = (element) => {
return {
width: element.clientWidth,
height: element.clientHeight,
};
}
window.getElementBoundingRect = (element) => {
const rect = element.getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y,
left: rect.left,
top: rect.top
};
}
window.setCanvasSize = (canvas, width, height) => {
canvas.width = width;
canvas.height = height;
}
window.imageCache = new Map();
window.preloadImage = (imagePath) => {
return new Promise((resolve, reject) => {
if (window.imageCache.has(imagePath)) {
resolve(window.imageCache.get(imagePath));
return;
}
const img = new Image();
img.onload = () => {
window.imageCache.set(imagePath, img);
resolve(img);
};
img.onerror = () => {
reject(new Error(`Failed to load image: ${imagePath}`));
};
img.src = imagePath;
});
};
window.preloadImageFromUrl = (url, cacheKey) => {
return new Promise((resolve, reject) => {
if (window.imageCache.has(cacheKey)) {
resolve(window.imageCache.get(cacheKey));
return;
}
const img = new Image();
img.onload = () => {
window.imageCache.set(cacheKey, img);
resolve(img);
};
img.onerror = (error) => {
reject(new Error(`Failed to load image from URL: ${url}`));
};
img.src = url;
});
};
window.getImageDimensions = (cacheKey) => {
const img = window.imageCache.get(cacheKey);
if (!img) {
return { width: 0, height: 0 };
}
return {
width: img.naturalWidth || img.width,
height: img.naturalHeight || img.height
};
};
window.drawImageOnCanvas = (canvas, imagePath, x, y, width, height) => {
const ctx = canvas.getContext('2d');
const img = window.imageCache.get(imagePath);
if (!img) {
return false;
}
try {
ctx.drawImage(img, x, y, width, height);
return true;
} catch (error) {
return false;
}
};
window.drawCachedImageOnCanvas = (canvas, cacheKey, x, y, width, height) => {
const ctx = canvas.getContext('2d');
const img = window.imageCache.get(cacheKey);
if (!img) {
return false;
}
try {
ctx.drawImage(img, x, y, width, height);
return true;
} catch (error) {
return false;
}
};

View File

@@ -0,0 +1,85 @@
// Helper functions for Robot Monitor
window.robotMonitor = {
// Get element size
getElementSize: function (element) {
const rect = element.getBoundingClientRect();
return {
Width: rect.width,
Height: rect.height
};
},
// Get element bounding rect
getElementBoundingRect: function (element) {
const rect = element.getBoundingClientRect();
return {
X: rect.x,
Y: rect.y,
Width: rect.width,
Height: rect.height
};
},
// Convert trajectory to SVG path
trajectoryToPath: function (trajectory, startX, startY, endX, endY) {
if (!trajectory || !trajectory.ControlPoints || trajectory.ControlPoints.length === 0) {
// Linear path
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
const degree = trajectory.Degree || 1;
const controlPoints = trajectory.ControlPoints;
if (degree === 1) {
// Linear
return `M ${startX} ${startY} L ${endX} ${endY}`;
} else if (degree === 2) {
// Quadratic bezier
if (controlPoints.length > 0) {
const cp1 = controlPoints[0];
return `M ${startX} ${startY} Q ${cp1.X} ${cp1.Y} ${endX} ${endY}`;
}
return `M ${startX} ${startY} L ${endX} ${endY}`;
} else if (degree === 3) {
// Cubic bezier
if (controlPoints.length >= 2) {
const cp1 = controlPoints[0];
const cp2 = controlPoints[1];
return `M ${startX} ${startY} C ${cp1.X} ${cp1.Y}, ${cp2.X} ${cp2.Y}, ${endX} ${endY}`;
} else if (controlPoints.length === 1) {
const cp1 = controlPoints[0];
return `M ${startX} ${startY} Q ${cp1.X} ${cp1.Y} ${endX} ${endY}`;
}
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
return `M ${startX} ${startY} L ${endX} ${endY}`;
},
// Load image and get dimensions
loadImageAndGetDimensions: function (imageUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve({
Width: img.naturalWidth || img.width,
Height: img.naturalHeight || img.height
});
};
img.onerror = () => {
reject(new Error(`Failed to load image: ${imageUrl}`));
};
img.src = imageUrl;
});
}
};

View File

@@ -0,0 +1,13 @@
namespace RobotApp.Common.Shares.Dtos;
public class MapDto
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public double Width { get; set; }
public double Height { get; set; }
public double Resolution { get; set; }
public double OriginX { get; set; }
public double OriginY { get; set; }
}

View File

@@ -0,0 +1,47 @@
using RobotApp.Common.Shares.Enums;
using System.ComponentModel.DataAnnotations;
namespace RobotApp.Common.Shares.Dtos;
#nullable disable
public record RobotConfigDto
{
public Guid Id { get; set; }
public NavigationType NavigationType { get; set; }
[Range(0.1, 10, ErrorMessage = "Value must be from 0.1 to 10")]
public double RadiusWheel { get; set; }
[Range(0.1, 10, ErrorMessage = "Value must be from 0.1 to 10")]
public double Width { get; set; }
[Range(0.1, 10, ErrorMessage = "Value must be from 0.1 to 10")]
public double Length { get; set; }
[Range(0.1, 10, ErrorMessage = "Value must be from 0.1 to 10")]
public double Height { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public bool IsActive { get; set; }
[Required]
public string ConfigName { get; set; }
public string Description { get; set; }
}
public record UpdateRobotConfigDto
{
public NavigationType NavigationType { get; set; }
public double RadiusWheel { get; set; }
public double Width { get; set; }
public double Length { get; set; }
public double Height { get; set; }
public string Description { get; set; }
}
public record CreateRobotConfigDto
{
public string ConfigName { get; set; }
public string Description { get; set; }
public NavigationType NavigationType { get; set; }
public double RadiusWheel { get; set; }
public double Width { get; set; }
public double Length { get; set; }
public double Height { get; set; }
}

View File

@@ -0,0 +1,31 @@
using RobotApp.VDA5050.State;
namespace RobotApp.Common.Shares.Dtos;
public class RobotMonitorDto
{
public RobotPositionDto RobotPosition { get; set; } = new();
public EdgeStateDto[] EdgeStates { get; set; } = [];
public NodeState[] NodeStates { get; set; } = [];
public bool HasOrder { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
public class RobotPositionDto
{
public double X { get; set; }
public double Y { get; set; }
public double Theta { get; set; }
}
public class EdgeStateDto()
{
public double StartX { get; set; }
public double StartY { get; set; }
public double EndX { get; set; }
public double EndY { get; set; }
public double ControlPoint1X { get; set; }
public double ControlPoint1Y { get; set; }
public double ControlPoint2X { get; set; }
public double ControlPoint2Y { get; set; }
public int Degree { get; set; }
}

View File

@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations;
namespace RobotApp.Common.Shares.Dtos;
#nullable disable
public record RobotPlcConfigDto
{
public Guid Id { get; set; }
[Required]
public string PLCAddress { get; set; }
[Range(1, 65535, ErrorMessage = "Value must be from 1 to 65535")]
public int PLCPort { get; set; }
[Range(1, 65535, ErrorMessage = "Value must be from 1 to 65535")]
public int PLCUnitId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public bool IsActive { get; set; }
[Required]
public string ConfigName { get; set; }
public string Description { get; set; }
}
public record UpdateRobotPlcConfigDto
{
public string Description { get; set; }
public string PLCAddress { get; set; }
public int PLCPort { get; set; }
public int PLCUnitId { get; set; }
}
public record CreateRobotPlcConfigDto
{
public string ConfigName { get; set; }
public string Description { get; set; }
public string PLCAddress { get; set; }
public int PLCPort { get; set; }
public int PLCUnitId { get; set; }
}

View File

@@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
namespace RobotApp.Common.Shares.Dtos;
#nullable disable
public record RobotSafetyConfigDto
{
public Guid Id { get; set; }
public double SafetySpeedVerySlow { get; set; }
[Range(0, 10, ErrorMessage = "Value must be from 0 to 10")]
public double SafetySpeedSlow { get; set; }
[Range(0, 10, ErrorMessage = "Value must be from 0 to 10")]
public double SafetySpeedNormal { get; set; }
[Range(0, 10, ErrorMessage = "Value must be from 0 to 10")]
public double SafetySpeedMedium { get; set; }
[Range(0, 10, ErrorMessage = "Value must be from 0 to 10")]
public double SafetySpeedOptimal { get; set; }
[Range(0, 10, ErrorMessage = "Value must be from 0 to 10")]
public double SafetySpeedFast { get; set; }
[Range(0, 10, ErrorMessage = "Value must be from 0 to 10")]
public double SafetySpeedVeryFast { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public bool IsActive { get; set; }
[Required]
public string ConfigName { get; set; }
public string Description { get; set; }
}
public record UpdateRobotSafetyConfigDto
{
public double SafetySpeedVerySlow { get; set; }
public double SafetySpeedSlow { get; set; }
public double SafetySpeedNormal { get; set; }
public double SafetySpeedMedium { get; set; }
public double SafetySpeedOptimal { get; set; }
public double SafetySpeedFast { get; set; }
public double SafetySpeedVeryFast { get; set; }
public string Description { get; set; }
}
public record CreateRobotSafetyConfigDto
{
public double SafetySpeedVerySlow { get; set; }
public double SafetySpeedSlow { get; set; }
public double SafetySpeedNormal { get; set; }
public double SafetySpeedMedium { get; set; }
public double SafetySpeedOptimal { get; set; }
public double SafetySpeedFast { get; set; }
public double SafetySpeedVeryFast { get; set; }
public string ConfigName { get; set; }
public string Description { get; set; }
}

View File

@@ -0,0 +1,46 @@
using System.ComponentModel.DataAnnotations;
namespace RobotApp.Common.Shares.Dtos;
#nullable disable
public record RobotSimulationConfigDto
{
public Guid Id { get; set; }
public bool EnableSimulation { get; set; }
[Range(0.1, 10, ErrorMessage = "Value must be from 0.1 to 10")]
public double SimulationMaxVelocity { get; set; }
[Range(0.1, 10, ErrorMessage = "Value must be from 0.1 to 10")]
public double SimulationMaxAngularVelocity { get; set; }
[Range(0.1, 10, ErrorMessage = "Value must be from 0.1 to 10")]
public double SimulationAcceleration { get; set; }
[Range(0.1, 10, ErrorMessage = "Value must be from 0.1 to 10")]
public double SimulationDeceleration { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public bool IsActive { get; set; }
[Required]
public string ConfigName { get; set; }
public string Description { get; set; }
}
public record UpdateRobotSimulationConfigDto
{
public bool EnableSimulation { get; set; }
public double SimulationMaxVelocity { get; set; }
public double SimulationMaxAngularVelocity { get; set; }
public double SimulationAcceleration { get; set; }
public double SimulationDeceleration { get; set; }
public string Description { get; set; }
}
public record CreateRobotSimulationConfigDto
{
public bool EnableSimulation { get; set; }
public double SimulationMaxVelocity { get; set; }
public double SimulationMaxAngularVelocity { get; set; }
public double SimulationAcceleration { get; set; }
public double SimulationDeceleration { get; set; }
public string ConfigName { get; set; }
public string Description { get; set; }
}

View File

@@ -0,0 +1,74 @@
using System.ComponentModel.DataAnnotations;
namespace RobotApp.Common.Shares.Dtos;
#nullable disable
public record RobotVDA5050ConfigDto
{
public Guid Id { get; set; }
[Required]
public string SerialNumber { get; set; }
[Required]
public string VDA5050HostServer { get; set; }
[Required]
[Range(1, 65535, ErrorMessage = "Value must be from 1 to 65535")]
public int VDA5050Port { get; set; }
public string VDA5050UserName { get; set; }
public string VDA5050Password { get; set; }
public string VDA5050Manufacturer { get; set; }
public string VDA5050Version { get; set; }
public string VDA5050TopicPrefix { get; set; }
[Range(1, 65535, ErrorMessage = "Value must be from 1 to 65535")]
public int VDA5050PublishRepeat { get; set; }
public bool VDA5050EnablePassword { get; set; }
public bool VDA5050EnableTls { get; set; }
public string VDA5050CA { get; set; }
public string VDA5050Cer { get; set; }
public string VDA5050Key { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public bool IsActive { get; set; }
[Required]
public string ConfigName { get; set; }
public string Description { get; set; }
}
public record UpdateRobotVDA5050ConfigDto
{
public string SerialNumber { get; set; }
public string VDA5050HostServer { get; set; }
public int VDA5050Port { get; set; }
public string VDA5050UserName { get; set; }
public string VDA5050Password { get; set; }
public string VDA5050Manufacturer { get; set; }
public string VDA5050Version { get; set; }
public string VDA5050TopicPrefix { get; set; }
public int VDA5050PublishRepeat { get; set; }
public bool VDA5050EnablePassword { get; set; }
public bool VDA5050EnableTls { get; set; }
public string VDA5050CA { get; set; }
public string VDA5050Cer { get; set; }
public string VDA5050Key { get; set; }
public string Description { get; set; }
}
public record CreateRobotVDA5050ConfigDto
{
public string SerialNumber { get; set; }
public string VDA5050HostServer { get; set; }
public int VDA5050Port { get; set; }
public string VDA5050UserName { get; set; }
public string VDA5050Password { get; set; }
public string VDA5050Manufacturer { get; set; }
public string VDA5050Version { get; set; }
public string VDA5050TopicPrefix { get; set; }
public int VDA5050PublishRepeat { get; set; }
public bool VDA5050EnablePassword { get; set; }
public bool VDA5050EnableTls { get; set; }
public string VDA5050CA { get; set; }
public string VDA5050Cer { get; set; }
public string VDA5050Key { get; set; }
public string ConfigName { get; set; }
public string Description { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace RobotApp.Common.Shares.Enums;
public enum RobotConfigType
{
VDA5050,
Core,
Safety ,
Simulation,
PLC,
}

View File

@@ -2,22 +2,29 @@
public enum RootStateType public enum RootStateType
{ {
Booting, System,
Operational, Auto,
Manual,
Service,
Stop,
Fault,
} }
public enum OperationalStateType public enum SystemStateType
{ {
Initializing,
Standby,
Shutting_Down,
} }
public enum AutomationStateType public enum AutoStateType
{ {
Idle, Idle,
Executing, Executing,
Paused, Paused,
Charging, Holding,
Error, Canceling,
Recovering,
Remote_Override, Remote_Override,
} }
@@ -27,13 +34,61 @@ public enum ManualStateType
Active, Active,
} }
public enum SafetyStateType public enum ServiceStateType
{ {
Init, Idle,
Run_Ok, Active,
SS1, }
STO,
PDS, public enum StopStateType
SLS, {
Error, EMC,
Bumber,
Protective,
Manual,
}
public enum FaultStateType
{
Navigation,
Localization,
Shielf,
Battery,
Driver,
Peripherals,
Safety,
Communication,
}
public enum ExecutingStateType
{
Planning,
Moving,
ACT,
}
public enum ACTStateType
{
Docking,
Docked,
Charging,
Undocking,
Loading,
Unloading,
TechAction,
}
public enum MoveStateType
{
Navigation,
Avoidance,
Approach,
Tracking,
Repositioning,
}
public enum PlanStateType
{
Task,
Path,
} }

View File

@@ -4,6 +4,14 @@ namespace RobotApp.Common.Shares;
public static class MathExtensions public static class MathExtensions
{ {
public static double NormalizeAngle(double angle)
{
angle = angle % 360;
if (angle > 180) angle -= 360;
else if (angle < -180) angle += 360;
return angle;
}
public static (double x, double y) CurveDegreeTwo(double t, double x1, double y1, double controlPointX, double controlPointY, double x2, double y2) public static (double x, double y) CurveDegreeTwo(double t, double x1, double y1, double controlPointX, double controlPointY, double x2, double y2)
{ {
var x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * controlPointX + t * t * x2; var x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * controlPointX + t * t * x2;
@@ -77,4 +85,26 @@ public static class MathExtensions
} }
return distance; return distance;
} }
public static double GetVectorAngle(double originNodeX, double originNodeY, double vector1X, double vector1Y, double vector2X, double vector2Y)
{
double BA_x = vector1X - originNodeX;
double BA_y = vector1Y - originNodeY;
double BC_x = vector2X - originNodeX;
double BC_y = vector2Y - originNodeY;
// Tính độ dài của các vector AB và BC
double lengthAB = Math.Sqrt(BA_x * BA_x + BA_y * BA_y);
double lengthBC = Math.Sqrt(BC_x * BC_x + BC_y * BC_y);
// Tính tích vô hướng của AB và BC
double dotProduct = BA_x * BC_x + BA_y * BC_y;
if (lengthAB * lengthBC == 0) return 0;
if (dotProduct / (lengthAB * lengthBC) > 1) return 0;
if (dotProduct / (lengthAB * lengthBC) < -1) return 180;
return Math.Acos(dotProduct / (lengthAB * lengthBC)) * (180.0 / Math.PI);
}
public static double Distance(double x1, double y1, double x2, double y2)
{
return Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2));
}
} }

View File

@@ -1,9 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -9,7 +9,8 @@ public enum ActionScopes
NODE, NODE,
EDGE, EDGE,
} }
public class AgvActions
public class AgvAction
{ {
[Required] [Required]
public string ActionType { get; set; } = string.Empty; public string ActionType { get; set; } = string.Empty;

View File

@@ -8,5 +8,5 @@ public class ProtocolFeatures
[Required] [Required]
public OptionalParameters[] OptionalParameters { get; set; } = []; public OptionalParameters[] OptionalParameters { get; set; } = [];
[Required] [Required]
public AgvActions[] AgvActions { get; set; } = []; public AgvAction[] AgvActions { get; set; } = [];
} }

View File

@@ -2,7 +2,6 @@
namespace RobotApp.VDA5050.Order; namespace RobotApp.VDA5050.Order;
#nullable disable
public class Edge public class Edge
{ {
@@ -26,7 +25,7 @@ public class Edge
public bool RotationAllowed { get; set; } public bool RotationAllowed { get; set; }
public double MaxRotationSpeed { get; set; } public double MaxRotationSpeed { get; set; }
public double Length { get; set; } public double Length { get; set; }
public Trajectory Trajectory { get; set; } public Trajectory? Trajectory { get; set; }
public Corridor Corridor { get; set; } = new(); public Corridor Corridor { get; set; } = new();
[Required] [Required]
public InstantAction.Action[] Actions { get; set; } = []; public InstantAction.Action[] Actions { get; set; } = [];

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@@ -13,5 +13,5 @@ public class EdgeState
public string EdgeDescription { get; set; } = string.Empty; public string EdgeDescription { get; set; } = string.Empty;
[Required] [Required]
public bool Released { get; set; } public bool Released { get; set; }
public Trajectory Trajectory { get; set; } = new(); public Trajectory? Trajectory { get; set; }
} }

View File

@@ -20,6 +20,7 @@ public class ErrorReferences
public enum ErrorType public enum ErrorType
{ {
INITIALIZE_ORDER, INITIALIZE_ORDER,
READ_PERIPHERAL_FAILURE,
} }
public class Error public class Error

View File

@@ -2,16 +2,32 @@
public enum ActionType public enum ActionType
{ {
START_PAUSE, startPause,
STOP_PAUSE, stopPause,
START_CHARGING, startCharging,
STOP_CHARGING, stopCharging,
INITIALIZATION_POSITION, initPosition,
PICK, stateRequest,
DROP, factsheetRequest,
CANCEL_ORDER,
ROTATE, //logReport,
REQUEST_FACTSHEET, pick,
REQUEST_VISUALIZATION, drop,
REQUEST_STATE, //detectObject,
//finePositioning,
//waitForTrigger,
cancelOrder,
//liftUp,
//liftDown,
//liftRotate,
rotate,
//rotateKeepLift,
//mutedBaseOn,
//mutedBaseOff,
//mutedLoadOn,
//mutedLoadOff,
//dockTo,
//moveStraightToCoor,
//moveStraightWithDistance,
} }

View File

@@ -0,0 +1,12 @@
namespace RobotApp.VDA5050.Type;
public enum InformationType
{
robot_general
}
public enum InformationReferencesKey
{
robot_state,
}

View File

@@ -1,5 +1,6 @@
namespace RobotApp.VDA5050; namespace RobotApp.VDA5050;
public class VDA5050Setting public class VDA5050Setting
{ {
public string HostServer { get; set; } = string.Empty; public string HostServer { get; set; } = string.Empty;
@@ -7,6 +8,12 @@ public class VDA5050Setting
public string UserName { get; set; } = "robotics"; public string UserName { get; set; } = "robotics";
public string Password { get; set; } = "robotics"; public string Password { get; set; } = "robotics";
public string Manufacturer { get; set; } = "PhenikaaX"; public string Manufacturer { get; set; } = "PhenikaaX";
public string Version { get; set; } = "0.0.1"; public string Version { get; set; } = "2.1.0";
public int PublishRepeat { get; set; } = 2; public int PublishRepeat { get; set; } = 2;
public bool EnablePassword { get; set; } = false;
public bool EnableTls { get; set; } = false;
public string? CAFile { get; set; }
public string? CerFile { get; set; }
public string? KeyFile { get; set; }
public string? TopicPrefix { get; set; }
} }

View File

@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 18
VisualStudioVersion = 17.12.35707.178 VisualStudioVersion = 18.0.11205.157
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}"
EndProject EndProject
@@ -37,4 +37,7 @@ Global
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CCB0B2E5-3C19-4B2E-B229-08A74F6EF27D}
EndGlobalSection
EndGlobal EndGlobal

View File

@@ -18,7 +18,9 @@ namespace RobotApp.Components.Account
[DoesNotReturn] [DoesNotReturn]
public void RedirectTo(string? uri) public void RedirectTo(string? uri)
{ {
uri ??= ""; try
{
uri ??= "/";
// Prevent open redirects. // Prevent open redirects.
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
@@ -31,6 +33,8 @@ namespace RobotApp.Components.Account
navigationManager.NavigateTo(uri); navigationManager.NavigateTo(uri);
throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering."); throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering.");
} }
catch (NavigationException) { }
}
[DoesNotReturn] [DoesNotReturn]
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters) public void RedirectTo(string uri, Dictionary<string, object?> queryParameters)

View File

@@ -4,23 +4,23 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RobotApp</title>
<base href="/" /> <base href="/" />
<link rel="stylesheet" href="@Assets["lib/bootstrap/css/bootstrap.min.css"]" /> <link rel="stylesheet" href="@Assets["lib/bootstrap/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["lib/mdi/font/css/materialdesignicons.min.css"]" /> <link rel="stylesheet" href="@Assets["lib/mdi/font/css/materialdesignicons.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" /> <link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["RobotApp.styles.css"]" /> <link rel="stylesheet" href="@Assets["RobotApp.styles.css"]" />
<link rel="stylesheet" href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" /> <link rel="stylesheet" href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/svg+xml" href="favicon.svg" />
<ImportMap /> <ImportMap />
<ImportMap @rendermode="InteractiveServer" /> <HeadOutlet />
<HeadOutlet @rendermode="InteractiveServer" />
</head> </head>
<body> <body>
<CascadingAuthenticationState> <CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly"> <Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData"> <Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(RobotApp.Client.Layout.MainLayout)"> <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(RobotApp.Client.MainLayout)">
<NotAuthorized> <NotAuthorized>
<RedirectToLogin /> <RedirectToLogin />
</NotAuthorized> </NotAuthorized>
@@ -28,7 +28,7 @@
</Found> </Found>
<NotFound> <NotFound>
<PageTitle>Not found</PageTitle> <PageTitle>Not found</PageTitle>
<LayoutView Layout="typeof(RobotApp.Client.Layout.MainLayout)"> <LayoutView Layout="typeof(RobotApp.Client.MainLayout)">
<p>Không tìm thấy trang.</p> <p>Không tìm thấy trang.</p>
</LayoutView> </LayoutView>
</NotFound> </NotFound>
@@ -39,7 +39,10 @@
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
<script src="@Assets["lib/bootstrap/js/bootstrap.bundle.min.js"]"></script> <script src="@Assets["lib/bootstrap/js/bootstrap.bundle.min.js"]"></script>
<script src="@Assets["lib/bootstrap/js/bootstrap.min.js"]"></script> <script src="@Assets["lib/bootstrap/js/bootstrap.min.js"]"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script> <script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
<script src="@Assets["js/canvas.js"]"></script>
<script src="@Assets["js/app.js"]"></script>
<script src="@Assets["js/robotMonitor.js"]"></script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,15 @@
@page "/"
@using Microsoft.AspNetCore.Authorization
@rendermode InteractiveServer
@inject NavigationManager Nav
@code
{
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (firstRender) Nav.NavigateTo("/dashboard", forceLoad: true);
}
}

View File

@@ -0,0 +1,12 @@
@inherits LayoutComponentBase
@layout RobotApp.Client.MainLayout
<MudThemeProvider @rendermode="InteractiveServer" IsDarkMode/>
<MudPopoverProvider @rendermode="InteractiveServer" />
<MudDialogProvider @rendermode="InteractiveServer" MaxWidth="MaxWidth.ExtraLarge"
CloseButton="false"
BackdropClick="false"
Position="DialogPosition.Center" />
<MudSnackbarProvider @rendermode="InteractiveServer" />
@Body

View File

@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RobotApp.Common.Shares;
namespace RobotApp.Controllers;
[Route("api/[controller]")]
[ApiController]
//[Authorize]
[AllowAnonymous]
public class FileController(Services.Logger<FileController> Logger) : ControllerBase
{
private readonly string certificatesPath = "MqttCertificates";
private readonly string caFilePath = "ca";
private readonly string cerFilePath = "cer";
private readonly string keyFilePath = "key";
[HttpPost]
[Route("certificates")]
public async Task<MessageResult> UpdateMqttCertificates([FromForm(Name = "CaFile")] IFormFile? caFile,
[FromForm(Name = "CertFile")] IFormFile? certFile,
[FromForm(Name = "KeyFile")] IFormFile? keyFile)
{
try
{
if (!Directory.Exists(certificatesPath)) Directory.CreateDirectory(certificatesPath);
if (caFile is not null)
{
var caFolder = Path.Combine(certificatesPath, caFilePath);
if (!Directory.Exists(caFolder)) Directory.CreateDirectory(caFolder);
string caExtension = Path.GetExtension(caFile.FileName);
var caLocal = Path.Combine(caFolder, caFile.FileName);
if (System.IO.File.Exists($"{caLocal}.bk")) System.IO.File.Delete($"{caLocal}.bk");
if (System.IO.File.Exists(caLocal)) System.IO.File.Move(caLocal, $"{caLocal}.bk");
using Stream fileStream = new FileStream(caLocal, FileMode.Create);
await caFile.OpenReadStream().CopyToAsync(fileStream);
}
if (certFile is not null)
{
var certFolder = Path.Combine(certificatesPath, cerFilePath);
if (!Directory.Exists(certFolder)) Directory.CreateDirectory(certFolder);
string certExtension = Path.GetExtension(certFile.FileName);
var certLocal = Path.Combine(certFolder, certFile.FileName);
if (System.IO.File.Exists($"{certLocal}.bk")) System.IO.File.Delete($"{certLocal}.bk");
if (System.IO.File.Exists(certLocal)) System.IO.File.Move(certLocal, $"{certLocal}.bk");
using Stream fileStream = new FileStream(certLocal, FileMode.Create);
await certFile.OpenReadStream().CopyToAsync(fileStream);
}
if (keyFile is not null)
{
var keyFolder = Path.Combine(certificatesPath, keyFilePath);
if (!Directory.Exists(keyFolder)) Directory.CreateDirectory(keyFolder);
string keyExtension = Path.GetExtension(keyFile.FileName);
var keyLocal = Path.Combine(keyFolder, keyFile.FileName);
if (System.IO.File.Exists($"{keyLocal}.bk")) System.IO.File.Delete($"{keyLocal}.bk");
if (System.IO.File.Exists(keyLocal)) System.IO.File.Move(keyLocal, $"{keyLocal}.bk");
using Stream fileStream = new FileStream(keyLocal, FileMode.Create);
await keyFile.OpenReadStream().CopyToAsync(fileStream);
}
return new(true, "");
}
catch (Exception ex)
{
Logger.Error($"Update Mqtt Certificates is failed: {ex.Message}");
return new(false, "An error occurred while retrieving update Mqtt Certificates.");
}
}
}

View File

@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace RobotApp.Controllers;
[Route("api/[controller]")]
[ApiController]
//[Authorize]
[AllowAnonymous]
public class ImagesController(Services.Logger<ImagesController> Logger) : ControllerBase
{
[HttpGet]
[Route("mapping")]
public async Task<IActionResult> GetMapImage()
{
try
{
await Task.Delay(1);
string fileName = "gara20250309.png";
string filePath = Path.Combine("maps", fileName);
if (System.IO.File.Exists(filePath))
{
byte[] imageBytes = await System.IO.File.ReadAllBytesAsync(filePath);
return File(imageBytes, "image/png");
}
else
{
string mapsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "maps");
if (Directory.Exists(mapsDir))
{
var pngFiles = Directory.GetFiles(mapsDir, "*.png");
return NotFound(new
{
error = "Map image not found",
searchPath = filePath,
mapsDirectory = mapsDir,
availableFiles = pngFiles.Select(Path.GetFileName).ToArray(),
baseDirectory = AppDomain.CurrentDomain.BaseDirectory
});
}
else
{
Logger.Warning($"GetMapImage: Maps directory does not exist: {mapsDir}");
return NotFound(new
{
error = "Maps directory not found",
searchPath = mapsDir,
baseDirectory = AppDomain.CurrentDomain.BaseDirectory
});
}
}
}
catch (Exception ex)
{
Logger.Warning($"GetMapImage: Exception occurred - {ex.Message}");
return StatusCode(500, new { error = "Internal server error", message = ex.Message, stackTrace = ex.StackTrace });
}
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace RobotApp.Controllers;
[Route("api/[controller]")]
[ApiController]
//[Authorize]
[AllowAnonymous]
public class LogsManagerController(Services.Logger<LogsManagerController> Logger) : ControllerBase
{
private readonly string LoggerDirectory = "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: phát hiện đường dẫn không hợp lệ.");
return [];
}
if (!System.IO.File.Exists(path))
{
Logger.Warning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}.");
return [];
}
temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log");
System.IO.File.Copy(path, temp);
return await System.IO.File.ReadAllLinesAsync(temp);
}
catch (Exception ex)
{
Logger.Warning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}");
return [];
}
finally
{
if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
}
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RobotApp.Interfaces;
using RobotApp.VDA5050.Order;
using System.Text.Json;
namespace RobotApp.Controllers;
[ApiController]
[Route("api/order")]
//[Authorize]
[AllowAnonymous]
public class OrderController(IOrder robotOrderController, IInstantActions instantActions) : ControllerBase
{
[HttpPost]
public IActionResult SendOrder([FromBody] OrderMsg order)
{
robotOrderController.UpdateOrder(order);
return Ok(new
{
success = true,
message = "Order received"
});
}
[HttpPost("cancel")]
public IActionResult CancelOrder()
{
robotOrderController.StopOrder();
instantActions.StopOrderAction();
return Ok(new
{
success = true,
message = "Order and actions have been cancelled"
});
}
}

View File

@@ -0,0 +1,864 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotApp.Common.Shares;
using RobotApp.Common.Shares.Dtos;
using RobotApp.Data;
using RobotApp.Services.Robot;
namespace RobotApp.Controllers;
[Route("api/[controller]")]
[ApiController]
//[Authorize]
[AllowAnonymous]
public class RobotConfigsController(Services.Logger<RobotConfigsController> Logger, ApplicationDbContext AppDb, RobotConfiguration RobotConfiguration) : ControllerBase
{
[HttpGet]
[Route("plc")]
public async Task<MessageResult<RobotPlcConfigDto[]>> GetAllPLCConfigs()
{
try
{
var configs = await AppDb.RobotPlcConfigs
.OrderByDescending(c => c.UpdatedAt)
.ToListAsync();
if (configs is null || configs.Count == 0) return new(false, "PLC configuration not found.");
return new(true)
{
Data = [.. configs.Select(config => new RobotPlcConfigDto
{
Id = config.Id,
ConfigName = config.ConfigName,
Description = config.Description,
PLCAddress = config.PLCAddress,
PLCPort = config.PLCPort,
PLCUnitId = config.PLCUnitId,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
})]
};
}
catch (Exception ex)
{
Logger.Error($"Error in Get PLC Configs: {ex.Message}");
return new(false, "An error occurred while retrieving the PLC configuration.");
}
}
[HttpPut]
[Route("plc/{id:guid}")]
public async Task<MessageResult> UpdatePLCConfig(Guid id, [FromBody] UpdateRobotPlcConfigDto updateDto)
{
try
{
var config = await AppDb.RobotPlcConfigs.FindAsync(id);
if (config is null) return new(false, "PLC configuration not found.");
config.Description = updateDto.Description ?? config.Description;
config.PLCAddress = updateDto.PLCAddress ?? config.PLCAddress;
config.PLCPort = updateDto.PLCPort;
config.PLCUnitId = (byte)updateDto.PLCUnitId;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, "PLC configuration updated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update PLC Config: {ex.Message}");
return new(false, "An error occurred while updating the PLC configuration.");
}
}
[HttpPost]
[Route("plc")]
public async Task<MessageResult<RobotPlcConfigDto>> CreatePLCConfig([FromBody] CreateRobotPlcConfigDto createDto)
{
try
{
if (string.IsNullOrEmpty(createDto.ConfigName)) return new(false, "ConfigName cannot be null or empty.");
if (await AppDb.RobotPlcConfigs.AnyAsync(cf => cf.ConfigName == createDto.ConfigName))
return new(false, "A PLC configuration with the same name already exists.");
var config = new RobotPlcConfig
{
ConfigName = createDto.ConfigName,
Description = createDto.Description,
PLCAddress = createDto.PLCAddress,
PLCPort = createDto.PLCPort,
PLCUnitId = (byte)createDto.PLCUnitId,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
IsActive = false
};
AppDb.RobotPlcConfigs.Add(config);
await AppDb.SaveChangesAsync();
return new(true, "PLC configuration created successfully.")
{
Data = new RobotPlcConfigDto
{
Id = config.Id,
ConfigName = config.ConfigName,
Description = config.Description,
PLCAddress = config.PLCAddress,
PLCPort = config.PLCPort,
PLCUnitId = config.PLCUnitId,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
}
};
}
catch (Exception ex)
{
Logger.Error($"Error in Create PLC Config: {ex.Message}");
return new(false, "An error occurred while creating the PLC configuration.");
}
}
[HttpDelete]
[Route("plc/{id:guid}")]
public async Task<MessageResult> DeletePLCConfig(Guid id)
{
try
{
var config = await AppDb.RobotPlcConfigs.FindAsync(id);
if (config is null) return new(false, "PLC configuration not found.");
if (config.IsActive) return new(false, "Cannot delete an active PLC configuration.");
if (config.ConfigName == "Default") return new(false, "Cannot delete the default PLC configuration.");
AppDb.RobotPlcConfigs.Remove(config);
await AppDb.SaveChangesAsync();
return new(true, "Configuration deleted successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Delete PLC Config: {ex.Message}");
return new(false, "An error occurred while deleting the PLC configuration.");
}
}
[HttpPut]
[Route("plc/active/{id:guid}")]
public async Task<MessageResult> ActivePLCConfig(Guid id)
{
try
{
var config = await AppDb.RobotPlcConfigs.FindAsync(id);
if (config is null) return new(false, "PLC configuration not found.");
await AppDb.RobotPlcConfigs.ExecuteUpdateAsync(cf => cf.SetProperty(c => c.IsActive, false));
config.IsActive = true;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, $"PLC configuration {config.ConfigName} activated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update Active PLC Config: {ex.Message}");
return new(false, "An error occurred while updating the active PLC configuration.");
}
}
[HttpGet]
[Route("vda5050")]
public async Task<MessageResult<RobotVDA5050ConfigDto[]>> GetAllVDA5050Configs()
{
try
{
var configs = await AppDb.RobotVDA5050Configs
.OrderByDescending(c => c.UpdatedAt)
.ToListAsync();
if (configs is null || configs.Count == 0) return new(false, "VDA5050 configuration not found.");
var configDtos = configs.Select(config => new RobotVDA5050ConfigDto
{
Id = config.Id,
SerialNumber = config.SerialNumber,
VDA5050HostServer = config.VDA5050HostServer,
VDA5050Port = config.VDA5050Port,
VDA5050UserName = config.VDA5050UserName,
VDA5050Password = config.VDA5050Password,
VDA5050Manufacturer = config.VDA5050Manufacturer,
VDA5050Version = config.VDA5050Version,
VDA5050TopicPrefix = config.VDA5050TopicPrefix,
VDA5050PublishRepeat = config.VDA5050PublishRepeat,
VDA5050EnablePassword = config.VDA5050EnablePassword,
VDA5050EnableTls = config.VDA5050EnableTls,
VDA5050CA = config.VDA5050CA,
VDA5050Cer = config.VDA5050Cer,
VDA5050Key = config.VDA5050Key,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
ConfigName = config.ConfigName,
Description = config.Description
}).ToArray();
return new(true) { Data = configDtos };
}
catch (Exception ex)
{
Logger.Error($"Error in Get All VDA5050 Configs: {ex.Message}");
return new(false, "An error occurred while retrieving VDA5050 configurations.");
}
}
[HttpPut]
[Route("vda5050/{id:guid}")]
public async Task<MessageResult> UpdateVDA5050Config(Guid id, [FromBody] UpdateRobotVDA5050ConfigDto updateDto)
{
try
{
var config = await AppDb.RobotVDA5050Configs.FindAsync(id);
if (config is null) return new(false, "VDA5050 configuration not found.");
config.SerialNumber = updateDto.SerialNumber ?? config.SerialNumber;
config.VDA5050HostServer = updateDto.VDA5050HostServer ?? config.VDA5050HostServer;
config.VDA5050Port = updateDto.VDA5050Port;
config.VDA5050UserName = updateDto.VDA5050UserName;
config.VDA5050Password = updateDto.VDA5050Password;
config.VDA5050Manufacturer = updateDto.VDA5050Manufacturer ?? config.VDA5050Manufacturer;
config.VDA5050Version = updateDto.VDA5050Version ?? config.VDA5050Version;
config.VDA5050TopicPrefix = updateDto.VDA5050TopicPrefix;
config.VDA5050PublishRepeat = updateDto.VDA5050PublishRepeat;
config.VDA5050EnablePassword = updateDto.VDA5050EnablePassword;
config.VDA5050EnableTls = updateDto.VDA5050EnableTls;
config.VDA5050CA = updateDto.VDA5050CA;
config.VDA5050Cer = updateDto.VDA5050Cer;
config.VDA5050Key = updateDto.VDA5050Key;
config.Description = updateDto.Description ?? config.Description;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, "VDA5050 configuration updated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update VDA5050 Config: {ex.Message}");
return new(false, "An error occurred while updating the VDA5050 configuration.");
}
}
[HttpPost]
[Route("vda5050")]
public async Task<MessageResult<RobotVDA5050ConfigDto>> CreateVDA5050Config([FromBody] CreateRobotVDA5050ConfigDto createDto)
{
try
{
if (string.IsNullOrEmpty(createDto.ConfigName)) return new(false, "ConfigName cannot be null or empty.");
if (await AppDb.RobotVDA5050Configs.AnyAsync(cf => cf.ConfigName == createDto.ConfigName))
return new(false, "A VDA5050 configuration with the same name already exists.");
var config = new RobotVDA5050Config
{
ConfigName = createDto.ConfigName,
Description = createDto.Description,
SerialNumber = createDto.SerialNumber,
VDA5050HostServer = createDto.VDA5050HostServer,
VDA5050Port = createDto.VDA5050Port,
VDA5050UserName = createDto.VDA5050UserName,
VDA5050Password = createDto.VDA5050Password,
VDA5050Manufacturer = createDto.VDA5050Manufacturer,
VDA5050Version = createDto.VDA5050Version,
VDA5050TopicPrefix = createDto.VDA5050TopicPrefix,
VDA5050PublishRepeat = createDto.VDA5050PublishRepeat,
VDA5050EnablePassword = createDto.VDA5050EnablePassword,
VDA5050EnableTls = createDto.VDA5050EnableTls,
VDA5050CA = createDto.VDA5050CA,
VDA5050Cer = createDto.VDA5050Cer,
VDA5050Key = createDto.VDA5050Key,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
IsActive = false
};
AppDb.RobotVDA5050Configs.Add(config);
await AppDb.SaveChangesAsync();
return new(true, "VDA5050 configuration created successfully.")
{
Data = new RobotVDA5050ConfigDto
{
Id = config.Id,
ConfigName = config.ConfigName,
Description = config.Description,
SerialNumber = config.SerialNumber,
VDA5050HostServer = config.VDA5050HostServer,
VDA5050Port = config.VDA5050Port,
VDA5050UserName = config.VDA5050UserName,
VDA5050Password = config.VDA5050Password,
VDA5050Manufacturer = config.VDA5050Manufacturer,
VDA5050Version = config.VDA5050Version,
VDA5050TopicPrefix = config.VDA5050TopicPrefix,
VDA5050PublishRepeat = config.VDA5050PublishRepeat,
VDA5050EnablePassword = config.VDA5050EnablePassword,
VDA5050EnableTls = config.VDA5050EnableTls,
VDA5050CA = config.VDA5050CA,
VDA5050Cer = config.VDA5050Cer,
VDA5050Key = config.VDA5050Key,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
}
};
}
catch (Exception ex)
{
Logger.Error($"Error in Create VDA5050 Config: {ex.Message}");
return new(false, "An error occurred while creating the VDA5050 configuration.");
}
}
[HttpDelete]
[Route("vda5050/{id:guid}")]
public async Task<MessageResult> DeleteVDA5050Config(Guid id)
{
try
{
var config = await AppDb.RobotVDA5050Configs.FindAsync(id);
if (config is null) return new(false, "VDA5050 configuration not found.");
if (config.IsActive) return new(false, "Cannot delete an active VDA5050 configuration.");
if (config.ConfigName == "Default") return new(false, "Cannot delete the default PLC configuration.");
AppDb.RobotVDA5050Configs.Remove(config);
await AppDb.SaveChangesAsync();
return new(true, "Configuration deleted successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Delete VDA5050 Config: {ex.Message}");
return new(false, "An error occurred while deleting the VDA5050 configuration.");
}
}
[HttpPut]
[Route("vda5050/active/{id:guid}")]
public async Task<MessageResult> ActiveVDA5050Config(Guid id)
{
try
{
var config = await AppDb.RobotVDA5050Configs.FindAsync(id);
if (config is null) return new(false, "VDA5050 configuration not found.");
await AppDb.RobotVDA5050Configs.ExecuteUpdateAsync(cf => cf.SetProperty(c => c.IsActive, false));
config.IsActive = true;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, $"VDA5050 configuration {config.ConfigName} activated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update Active VDA5050 Config: {ex.Message}");
return new(false, "An error occurred while updating the active VDA5050 configuration.");
}
}
[HttpGet]
[Route("robot")]
public async Task<MessageResult<RobotConfigDto[]>> GetAllRobotConfigs()
{
try
{
var configs = await AppDb.RobotConfigs
.OrderByDescending(c => c.UpdatedAt)
.ToListAsync();
if (configs is null || configs.Count == 0) return new(false, "Robot configuration not found.");
var configDtos = configs.Select(config => new RobotConfigDto
{
Id = config.Id,
NavigationType = config.NavigationType,
RadiusWheel = config.RadiusWheel,
Width = config.Width,
Length = config.Length,
Height = config.Height,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
ConfigName = config.ConfigName,
Description = config.Description
}).ToArray();
return new(true) { Data = configDtos };
}
catch (Exception ex)
{
Logger.Error($"Error in Get All Robot Configs: {ex.Message}");
return new(false, "An error occurred while retrieving Robot configurations.");
}
}
[HttpPut]
[Route("robot/{id:guid}")]
public async Task<MessageResult> UpdateRobotConfig(Guid id, [FromBody] UpdateRobotConfigDto updateDto)
{
try
{
var config = await AppDb.RobotConfigs.FindAsync(id);
if (config is null) return new(false, "Robot configuration not found.");
config.NavigationType = updateDto.NavigationType;
config.RadiusWheel = updateDto.RadiusWheel;
config.Width = updateDto.Width;
config.Length = updateDto.Length;
config.Height = updateDto.Height;
config.Description = updateDto.Description ?? config.Description;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, "Robot configuration updated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update Robot Config: {ex.Message}");
return new(false, "An error occurred while updating the Robot configuration.");
}
}
[HttpPost]
[Route("robot")]
public async Task<MessageResult<RobotConfigDto>> CreateRobotConfig([FromBody] CreateRobotConfigDto createDto)
{
try
{
if (string.IsNullOrEmpty(createDto.ConfigName)) return new(false, "ConfigName cannot be null or empty.");
if (await AppDb.RobotConfigs.AnyAsync(cf => cf.ConfigName == createDto.ConfigName))
return new(false, "A Robot configuration with the same name already exists.");
var config = new RobotConfig
{
ConfigName = createDto.ConfigName,
Description = createDto.Description,
NavigationType = createDto.NavigationType,
RadiusWheel = createDto.RadiusWheel,
Width = createDto.Width,
Length = createDto.Length,
Height = createDto.Height,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
IsActive = false
};
AppDb.RobotConfigs.Add(config);
await AppDb.SaveChangesAsync();
return new(true, "Robot configuration created successfully.")
{
Data = new RobotConfigDto
{
Id = config.Id,
ConfigName = config.ConfigName,
Description = config.Description,
NavigationType = config.NavigationType,
RadiusWheel = config.RadiusWheel,
Width = config.Width,
Length = config.Length,
Height = config.Height,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
}
};
}
catch (Exception ex)
{
Logger.Error($"Error in Create Robot Config: {ex.Message}");
return new(false, "An error occurred while creating the Robot configuration.");
}
}
[HttpDelete]
[Route("robot/{id:guid}")]
public async Task<MessageResult> DeleteRobotConfig(Guid id)
{
try
{
var config = await AppDb.RobotConfigs.FindAsync(id);
if (config is null) return new(false, "Robot configuration not found.");
if (config.IsActive) return new(false, "Cannot delete an active Robot configuration.");
if (config.ConfigName == "Default") return new(false, "Cannot delete the default PLC configuration.");
AppDb.RobotConfigs.Remove(config);
await AppDb.SaveChangesAsync();
return new(true, "Configuration deleted successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Delete Robot Config: {ex.Message}");
return new(false, "An error occurred while deleting the Robot configuration.");
}
}
[HttpPut]
[Route("robot/active/{id:guid}")]
public async Task<MessageResult> ActiveRobotConfig(Guid id)
{
try
{
var config = await AppDb.RobotConfigs.FindAsync(id);
if (config is null) return new(false, "Robot configuration not found.");
await AppDb.RobotConfigs.ExecuteUpdateAsync(cf => cf.SetProperty(c => c.IsActive, false));
config.IsActive = true;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, $"Robot configuration {config.ConfigName} activated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update Active Robot Config: {ex.Message}");
return new(false, "An error occurred while updating the active Robot configuration.");
}
}
[HttpGet]
[Route("simulation")]
public async Task<MessageResult<RobotSimulationConfigDto[]>> GetAllSimulationConfigs()
{
try
{
var configs = await AppDb.RobotSimulationConfigs
.OrderByDescending(c => c.UpdatedAt)
.ToListAsync();
if (configs is null || configs.Count == 0) return new(false, "Simulation configuration not found.");
var configDtos = configs.Select(config => new RobotSimulationConfigDto
{
Id = config.Id,
EnableSimulation = config.EnableSimulation,
SimulationMaxVelocity = config.SimulationMaxVelocity,
SimulationMaxAngularVelocity = config.SimulationMaxAngularVelocity,
SimulationAcceleration = config.SimulationAcceleration,
SimulationDeceleration = config.SimulationDeceleration,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
ConfigName = config.ConfigName,
Description = config.Description
}).ToArray();
return new(true) { Data = configDtos };
}
catch (Exception ex)
{
Logger.Error($"Error in Get All Simulation Configs: {ex.Message}");
return new(false, "An error occurred while retrieving Simulation configurations.");
}
}
[HttpPut]
[Route("simulation/{id:guid}")]
public async Task<MessageResult> UpdateSimulationConfig(Guid id, [FromBody] UpdateRobotSimulationConfigDto updateDto)
{
try
{
var config = await AppDb.RobotSimulationConfigs.FindAsync(id);
if (config is null) return new(false, "Simulation configuration not found.");
config.EnableSimulation = updateDto.EnableSimulation;
config.SimulationMaxVelocity = updateDto.SimulationMaxVelocity;
config.SimulationMaxAngularVelocity = updateDto.SimulationMaxAngularVelocity;
config.SimulationAcceleration = updateDto.SimulationAcceleration;
config.SimulationDeceleration = updateDto.SimulationDeceleration;
config.Description = updateDto.Description ?? config.Description;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, "Simulation configuration updated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update Simulation Config: {ex.Message}");
return new(false, "An error occurred while updating the Simulation configuration.");
}
}
[HttpPost]
[Route("simulation")]
public async Task<MessageResult<RobotSimulationConfigDto>> CreateRobotSimulationConfig([FromBody] CreateRobotSimulationConfigDto createDto)
{
try
{
if (string.IsNullOrEmpty(createDto.ConfigName)) return new(false, "ConfigName cannot be null or empty.");
if (await AppDb.RobotSimulationConfigs.AnyAsync(cf => cf.ConfigName == createDto.ConfigName))
return new(false, "A Simulation configuration with the same name already exists.");
var config = new RobotSimulationConfig
{
ConfigName = createDto.ConfigName,
Description = createDto.Description,
EnableSimulation = createDto.EnableSimulation,
SimulationMaxVelocity = createDto.SimulationMaxVelocity,
SimulationMaxAngularVelocity = createDto.SimulationMaxAngularVelocity,
SimulationAcceleration = createDto.SimulationAcceleration,
SimulationDeceleration = createDto.SimulationDeceleration,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
IsActive = false
};
AppDb.RobotSimulationConfigs.Add(config);
await AppDb.SaveChangesAsync();
return new(true, "Simulation configuration created successfully.")
{
Data = new RobotSimulationConfigDto
{
Id = config.Id,
ConfigName = config.ConfigName,
Description = config.Description,
EnableSimulation = config.EnableSimulation,
SimulationMaxVelocity = config.SimulationMaxVelocity,
SimulationMaxAngularVelocity = config.SimulationMaxAngularVelocity,
SimulationAcceleration = config.SimulationAcceleration,
SimulationDeceleration = config.SimulationDeceleration,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
}
};
}
catch (Exception ex)
{
Logger.Error($"Error in Create Simulation Config: {ex.Message}");
return new(false, "An error occurred while creating the Simulation configuration.");
}
}
[HttpDelete]
[Route("simulation/{id:guid}")]
public async Task<MessageResult> DeleteRobotSimulationConfig(Guid id)
{
try
{
var config = await AppDb.RobotSimulationConfigs.FindAsync(id);
if (config is null) return new(false, "Simulation configuration not found.");
if (config.IsActive) return new(false, "Cannot delete an active Simulation configuration.");
if (config.ConfigName == "Default") return new(false, "Cannot delete the default PLC configuration.");
AppDb.RobotSimulationConfigs.Remove(config);
await AppDb.SaveChangesAsync();
return new(true, "Configuration deleted successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Delete Simulation Config: {ex.Message}");
return new(false, "An error occurred while deleting the Simulation configuration.");
}
}
[HttpPut]
[Route("simulation/active/{id:guid}")]
public async Task<MessageResult> ActiveSimulationConfig(Guid id)
{
try
{
var config = await AppDb.RobotSimulationConfigs.FindAsync(id);
if (config is null) return new(false, "Simulation configuration not found.");
await AppDb.RobotSimulationConfigs.ExecuteUpdateAsync(cf => cf.SetProperty(c => c.IsActive, false));
config.IsActive = true;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, $"Simulation configuration {config.ConfigName} activated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update Active Simulation Config: {ex.Message}");
return new(false, "An error occurred while updating the active Simulation configuration.");
}
}
[HttpGet]
[Route("safety")]
public async Task<MessageResult<RobotSafetyConfigDto[]>> GetAllSafetyConfigs()
{
try
{
var configs = await AppDb.RobotSafetyConfigs
.OrderByDescending(c => c.UpdatedAt)
.ToListAsync();
if (configs is null || configs.Count == 0) return new(false, "Safety configuration not found.");
var configDtos = configs.Select(config => new RobotSafetyConfigDto
{
Id = config.Id,
SafetySpeedVerySlow = config.SafetySpeedVerySlow,
SafetySpeedSlow = config.SafetySpeedSlow,
SafetySpeedNormal = config.SafetySpeedNormal,
SafetySpeedMedium = config.SafetySpeedMedium,
SafetySpeedOptimal = config.SafetySpeedOptimal,
SafetySpeedFast = config.SafetySpeedFast,
SafetySpeedVeryFast = config.SafetySpeedVeryFast,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
ConfigName = config.ConfigName,
Description = config.Description
}).ToArray();
return new(true) { Data = configDtos };
}
catch (Exception ex)
{
Logger.Error($"Error in Get All Safety Configs: {ex.Message}");
return new(false, "An error occurred while retrieving Safety configurations.");
}
}
[HttpPut]
[Route("safety/{id:guid}")]
public async Task<MessageResult> UpdateSafetyConfig(Guid id, [FromBody] UpdateRobotSafetyConfigDto updateDto)
{
try
{
var config = await AppDb.RobotSafetyConfigs.FindAsync(id);
if (config is null) return new(false, "Safety configuration not found.");
config.SafetySpeedVerySlow = updateDto.SafetySpeedVerySlow;
config.SafetySpeedSlow = updateDto.SafetySpeedSlow;
config.SafetySpeedNormal = updateDto.SafetySpeedNormal;
config.SafetySpeedMedium = updateDto.SafetySpeedMedium;
config.SafetySpeedOptimal = updateDto.SafetySpeedOptimal;
config.SafetySpeedFast = updateDto.SafetySpeedFast;
config.SafetySpeedVeryFast = updateDto.SafetySpeedVeryFast;
config.Description = updateDto.Description ?? config.Description;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, "Safety configuration updated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update Safety Config: {ex.Message}");
return new(false, "An error occurred while updating the Safety configuration.");
}
}
[HttpPost]
[Route("safety")]
public async Task<MessageResult<RobotSafetyConfigDto>> CreateRobotSafetyConfig([FromBody] CreateRobotSafetyConfigDto createDto)
{
try
{
if (string.IsNullOrEmpty(createDto.ConfigName)) return new(false, "ConfigName cannot be null or empty.");
if (await AppDb.RobotSafetyConfigs.AnyAsync(cf => cf.ConfigName == createDto.ConfigName))
return new(false, "A Safety configuration with the same name already exists.");
var config = new RobotSafetyConfig
{
ConfigName = createDto.ConfigName,
Description = createDto.Description,
SafetySpeedVerySlow = createDto.SafetySpeedVerySlow,
SafetySpeedSlow = createDto.SafetySpeedSlow,
SafetySpeedNormal = createDto.SafetySpeedNormal,
SafetySpeedMedium = createDto.SafetySpeedMedium,
SafetySpeedOptimal = createDto.SafetySpeedOptimal,
SafetySpeedFast = createDto.SafetySpeedFast,
SafetySpeedVeryFast = createDto.SafetySpeedVeryFast,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
IsActive = false
};
AppDb.RobotSafetyConfigs.Add(config);
await AppDb.SaveChangesAsync();
return new(true, "Safety configuration created successfully.")
{
Data = new RobotSafetyConfigDto
{
Id = config.Id,
ConfigName = config.ConfigName,
Description = config.Description,
SafetySpeedVerySlow = config.SafetySpeedVerySlow,
SafetySpeedSlow = config.SafetySpeedSlow,
SafetySpeedNormal = config.SafetySpeedNormal,
SafetySpeedMedium = config.SafetySpeedMedium,
SafetySpeedOptimal = config.SafetySpeedOptimal,
SafetySpeedFast = config.SafetySpeedFast,
SafetySpeedVeryFast = config.SafetySpeedVeryFast,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt,
IsActive = config.IsActive,
}
};
}
catch (Exception ex)
{
Logger.Error($"Error in Create Safety Config: {ex.Message}");
return new(false, "An error occurred while creating the Safety configuration.");
}
}
[HttpDelete]
[Route("safety/{id:guid}")]
public async Task<MessageResult> DeleteRobotSafetyConfig(Guid id)
{
try
{
var config = await AppDb.RobotSafetyConfigs.FindAsync(id);
if (config is null) return new(false, "Safety configuration not found.");
if (config.IsActive) return new(false, "Cannot delete an active Safety configuration.");
if (config.ConfigName == "Default") return new(false, "Cannot delete the default PLC configuration.");
AppDb.RobotSafetyConfigs.Remove(config);
await AppDb.SaveChangesAsync();
return new(true, "Configuration deleted successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Delete Safety Config: {ex.Message}");
return new(false, "An error occurred while deleting the Safety configuration.");
}
}
[HttpPut]
[Route("safety/active/{id:guid}")]
public async Task<MessageResult> ActiveSafetyConfig(Guid id)
{
try
{
var config = await AppDb.RobotSafetyConfigs.FindAsync(id);
if (config is null) return new(false, "Safety configuration not found.");
await AppDb.RobotSafetyConfigs.ExecuteUpdateAsync(cf => cf.SetProperty(c => c.IsActive, false));
config.IsActive = true;
config.UpdatedAt = DateTime.Now;
await AppDb.SaveChangesAsync();
return new(true, $"Safety configuration {config.ConfigName} activated successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Update Active Safety Config: {ex.Message}");
return new(false, "An error occurred while updating the active Safety configuration.");
}
}
[HttpPost]
[Route("load")]
public async Task<MessageResult> LoadConfig()
{
try
{
await RobotConfiguration.LoadVDA5050ConfigAsync();
await RobotConfiguration.LoadRobotSimulationConfigAsync();
return new(true, "Robot configuration loaded successfully.");
}
catch (Exception ex)
{
Logger.Error($"Error in Load Robot Config: {ex.Message}");
return new(false, "An error occurred while loading the Robot configuration.");
}
}
}

View File

@@ -3,10 +3,12 @@ using Microsoft.EntityFrameworkCore;
namespace RobotApp.Data namespace RobotApp.Data
{ {
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string> public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser, ApplicationRole, string>(options)
{ {
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) public DbSet<RobotConfig> RobotConfigs { get; private set; }
{ public DbSet<RobotSimulationConfig> RobotSimulationConfigs { get; private set; }
} public DbSet<RobotPlcConfig> RobotPlcConfigs { get; private set; }
public DbSet<RobotVDA5050Config> RobotVDA5050Configs { get; private set; }
public DbSet<RobotSafetyConfig> RobotSafetyConfigs { get; private set; }
} }
} }

View File

@@ -17,6 +17,7 @@ public static class ApplicationDbExtensions
await scope.ServiceProvider.SeedRolesAsync(); await scope.ServiceProvider.SeedRolesAsync();
await scope.ServiceProvider.SeedUsersAsync(); await scope.ServiceProvider.SeedUsersAsync();
await scope.ServiceProvider.SeedConfigAsync();
} }
private static async Task SeedRolesAsync(this IServiceProvider serviceProvider) private static async Task SeedRolesAsync(this IServiceProvider serviceProvider)
@@ -31,6 +32,15 @@ public static class ApplicationDbExtensions
NormalizedName = "ADMINISTRATOR", NormalizedName = "ADMINISTRATOR",
}); });
} }
if (!await roleManager.RoleExistsAsync("Distributor"))
{
await roleManager.CreateAsync(new ApplicationRole()
{
Name = "Distributor",
NormalizedName = "DISTRIBUTOR",
});
}
} }
private static async Task SeedUsersAsync(this IServiceProvider serviceProvider) private static async Task SeedUsersAsync(this IServiceProvider serviceProvider)
@@ -50,5 +60,131 @@ public static class ApplicationDbExtensions
await userManager.CreateAsync(admin, "robotics"); await userManager.CreateAsync(admin, "robotics");
await userManager.AddToRoleAsync(admin, "Administrator"); await userManager.AddToRoleAsync(admin, "Administrator");
} }
if (await userManager.FindByNameAsync("distributor") is null)
{
var distributor = new ApplicationUser()
{
UserName = "distributor",
Email = "distributor@phenikaa-x.com",
NormalizedUserName = "DISTRIBUTOR",
NormalizedEmail = "DISTRIBUTOR@PHENIKAA-X.COM",
EmailConfirmed = true,
};
await userManager.CreateAsync(distributor, "robotics");
await userManager.AddToRoleAsync(distributor, "Distributor");
}
}
private static async Task SeedConfigAsync(this IServiceProvider serviceProvider)
{
using var appDb = serviceProvider.GetRequiredService<ApplicationDbContext>();
if (!await appDb.RobotConfigs.AnyAsync())
{
var defaultConfig = new RobotConfig
{
ConfigName = "Default",
Description = "Default robot configuration",
NavigationType = Common.Shares.Enums.NavigationType.Differential,
RadiusWheel = 0.1,
Width = 0.6,
Length = 1.1,
Height = 0.5,
IsActive = true,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
};
appDb.RobotConfigs.Add(defaultConfig);
await appDb.SaveChangesAsync();
}
if (!await appDb.RobotPlcConfigs.AnyAsync())
{
var defaultConfig = new RobotPlcConfig
{
ConfigName = "Default",
Description = "Default robot PLC configuration",
PLCAddress = "127.0.0.1",
PLCPort = 502,
PLCUnitId = 1,
IsActive = true,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
};
appDb.RobotPlcConfigs.Add(defaultConfig);
await appDb.SaveChangesAsync();
}
if (!await appDb.RobotSimulationConfigs.AnyAsync())
{
var defaultConfig = new RobotSimulationConfig
{
ConfigName = "Default",
Description = "Default robot simulation configuration",
EnableSimulation = true,
SimulationMaxVelocity = 1.5,
SimulationMaxAngularVelocity = 0.5,
SimulationAcceleration = 2,
SimulationDeceleration = 10,
IsActive = true,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
};
appDb.RobotSimulationConfigs.Add(defaultConfig);
await appDb.SaveChangesAsync();
}
if (!await appDb.RobotVDA5050Configs.AnyAsync())
{
var defaultConfig = new RobotVDA5050Config
{
ConfigName = "Default",
Description = "Default robot VDA5050 configuration",
SerialNumber = "T800-002",
VDA5050HostServer = "127.0.0.1",
VDA5050Port = 1883,
VDA5050Version = "2.1.0",
VDA5050Manufacturer = "PhenikaaX",
VDA5050PublishRepeat = 2,
VDA5050EnablePassword = true,
VDA5050EnableTls = false,
VDA5050UserName = "robotics",
VDA5050Password = "robotics",
VDA5050TopicPrefix = "uagv/v2",
IsActive = true,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
};
appDb.RobotVDA5050Configs.Add(defaultConfig);
await appDb.SaveChangesAsync();
}
if (!await appDb.RobotSafetyConfigs.AnyAsync())
{
var defaultConfig = new RobotSafetyConfig
{
ConfigName = "Default",
Description = "Default robot Safety configuration",
SafetySpeedVerySlow = 0.15,
SafetySpeedSlow = 0.3,
SafetySpeedNormal = 0.6,
SafetySpeedMedium = 0.9,
SafetySpeedOptimal = 1.2,
SafetySpeedFast = 1.5,
SafetySpeedVeryFast = 1.9,
IsActive = true,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
};
appDb.RobotSafetyConfigs.Add(defaultConfig);
await appDb.SaveChangesAsync();
}
} }
} }

View File

@@ -1,279 +0,0 @@
// <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 RobotApp.Data;
#nullable disable
namespace RobotApp.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("00000000000000_CreateIdentitySchema")]
partial class CreateIdentitySchema
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("RobotApp.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("Text");
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("Text");
b.Property<string>("PhoneNumber")
.HasColumnType("Text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("Text");
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("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("Text");
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("Text");
b.Property<string>("ClaimValue")
.HasColumnType("Text");
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("Text");
b.Property<string>("ClaimValue")
.HasColumnType("Text");
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("Text");
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("Text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
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("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("RobotApp.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("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,224 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RobotApp.Migrations
{
/// <inheritdoc />
public partial class CreateIdentitySchema : 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: "Text", 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: "Text", nullable: true),
SecurityStamp = table.Column<string>(type: "Text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "Text", nullable: true),
PhoneNumber = table.Column<string>(type: "Text", 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: "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: "Text", nullable: true),
ClaimValue = table.Column<string>(type: "Text", 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: "Text", nullable: true),
ClaimValue = table.Column<string>(type: "Text", 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: "Text", 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: "Text", 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.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");
}
/// <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: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}

View File

@@ -1,268 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RobotApp.Data;
#nullable disable
namespace RobotApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250926020848_initDb")]
partial class initDb
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
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("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
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("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
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("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("RobotApp.Data.ApplicationRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("RobotApp.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,721 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RobotApp.Data.Migrations
{
/// <inheritdoc />
public partial class initDb : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "UserNameIndex",
table: "AspNetUsers");
migrationBuilder.DropIndex(
name: "RoleNameIndex",
table: "AspNetRoles");
migrationBuilder.AlterColumn<string>(
name: "Value",
table: "AspNetUserTokens",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "AspNetUserTokens",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "LoginProvider",
table: "AspNetUserTokens",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "AspNetUserTokens",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "UserName",
table: "AspNetUsers",
type: "TEXT",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "TwoFactorEnabled",
table: "AspNetUsers",
type: "INTEGER",
nullable: false,
oldClrType: typeof(bool),
oldType: "bit");
migrationBuilder.AlterColumn<string>(
name: "SecurityStamp",
table: "AspNetUsers",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "PhoneNumberConfirmed",
table: "AspNetUsers",
type: "INTEGER",
nullable: false,
oldClrType: typeof(bool),
oldType: "bit");
migrationBuilder.AlterColumn<string>(
name: "PhoneNumber",
table: "AspNetUsers",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "PasswordHash",
table: "AspNetUsers",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "NormalizedUserName",
table: "AspNetUsers",
type: "TEXT",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "NormalizedEmail",
table: "AspNetUsers",
type: "TEXT",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<DateTimeOffset>(
name: "LockoutEnd",
table: "AspNetUsers",
type: "TEXT",
nullable: true,
oldClrType: typeof(DateTimeOffset),
oldType: "datetimeoffset",
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "LockoutEnabled",
table: "AspNetUsers",
type: "INTEGER",
nullable: false,
oldClrType: typeof(bool),
oldType: "bit");
migrationBuilder.AlterColumn<bool>(
name: "EmailConfirmed",
table: "AspNetUsers",
type: "INTEGER",
nullable: false,
oldClrType: typeof(bool),
oldType: "bit");
migrationBuilder.AlterColumn<string>(
name: "Email",
table: "AspNetUsers",
type: "TEXT",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ConcurrencyStamp",
table: "AspNetUsers",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "AccessFailedCount",
table: "AspNetUsers",
type: "INTEGER",
nullable: false,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.AlterColumn<string>(
name: "Id",
table: "AspNetUsers",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "RoleId",
table: "AspNetUserRoles",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "AspNetUserRoles",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "AspNetUserLogins",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "ProviderDisplayName",
table: "AspNetUserLogins",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ProviderKey",
table: "AspNetUserLogins",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "LoginProvider",
table: "AspNetUserLogins",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "AspNetUserClaims",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "ClaimValue",
table: "AspNetUserClaims",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ClaimType",
table: "AspNetUserClaims",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "AspNetUserClaims",
type: "INTEGER",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("Sqlite:Autoincrement", true)
.OldAnnotation("Sqlite:Autoincrement", true);
migrationBuilder.AlterColumn<string>(
name: "NormalizedName",
table: "AspNetRoles",
type: "TEXT",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "AspNetRoles",
type: "TEXT",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ConcurrencyStamp",
table: "AspNetRoles",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Id",
table: "AspNetRoles",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "RoleId",
table: "AspNetRoleClaims",
type: "TEXT",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(450)");
migrationBuilder.AlterColumn<string>(
name: "ClaimValue",
table: "AspNetRoleClaims",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ClaimType",
table: "AspNetRoleClaims",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "Text",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "AspNetRoleClaims",
type: "INTEGER",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("Sqlite:Autoincrement", true)
.OldAnnotation("Sqlite:Autoincrement", true);
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "UserNameIndex",
table: "AspNetUsers");
migrationBuilder.DropIndex(
name: "RoleNameIndex",
table: "AspNetRoles");
migrationBuilder.AlterColumn<string>(
name: "Value",
table: "AspNetUserTokens",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "AspNetUserTokens",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "LoginProvider",
table: "AspNetUserTokens",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "AspNetUserTokens",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "UserName",
table: "AspNetUsers",
type: "nvarchar(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "TwoFactorEnabled",
table: "AspNetUsers",
type: "bit",
nullable: false,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<string>(
name: "SecurityStamp",
table: "AspNetUsers",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "PhoneNumberConfirmed",
table: "AspNetUsers",
type: "bit",
nullable: false,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<string>(
name: "PhoneNumber",
table: "AspNetUsers",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "PasswordHash",
table: "AspNetUsers",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "NormalizedUserName",
table: "AspNetUsers",
type: "nvarchar(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "NormalizedEmail",
table: "AspNetUsers",
type: "nvarchar(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<DateTimeOffset>(
name: "LockoutEnd",
table: "AspNetUsers",
type: "datetimeoffset",
nullable: true,
oldClrType: typeof(DateTimeOffset),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "LockoutEnabled",
table: "AspNetUsers",
type: "bit",
nullable: false,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<bool>(
name: "EmailConfirmed",
table: "AspNetUsers",
type: "bit",
nullable: false,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<string>(
name: "Email",
table: "AspNetUsers",
type: "nvarchar(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ConcurrencyStamp",
table: "AspNetUsers",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "AccessFailedCount",
table: "AspNetUsers",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<string>(
name: "Id",
table: "AspNetUsers",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "RoleId",
table: "AspNetUserRoles",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "AspNetUserRoles",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "AspNetUserLogins",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "ProviderDisplayName",
table: "AspNetUserLogins",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ProviderKey",
table: "AspNetUserLogins",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "LoginProvider",
table: "AspNetUserLogins",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "AspNetUserClaims",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "ClaimValue",
table: "AspNetUserClaims",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ClaimType",
table: "AspNetUserClaims",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "AspNetUserClaims",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER")
.Annotation("Sqlite:Autoincrement", true)
.OldAnnotation("Sqlite:Autoincrement", true);
migrationBuilder.AlterColumn<string>(
name: "NormalizedName",
table: "AspNetRoles",
type: "nvarchar(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "AspNetRoles",
type: "nvarchar(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ConcurrencyStamp",
table: "AspNetRoles",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Id",
table: "AspNetRoles",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "RoleId",
table: "AspNetRoleClaims",
type: "nvarchar(450)",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "ClaimValue",
table: "AspNetRoleClaims",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ClaimType",
table: "AspNetRoleClaims",
type: "Text",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "AspNetRoleClaims",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER")
.Annotation("Sqlite:Autoincrement", true)
.OldAnnotation("Sqlite:Autoincrement", true);
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true,
filter: "[NormalizedUserName] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true,
filter: "[NormalizedName] IS NOT NULL");
}
}
}

View File

@@ -0,0 +1,582 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RobotApp.Data;
#nullable disable
namespace RobotApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251222091852_InitApplicationDb")]
partial class InitApplicationDb
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
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("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
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("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
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("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("RobotApp.Data.ApplicationRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("RobotApp.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("RobotApp.Data.RobotConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<double>("Height")
.HasColumnType("float")
.HasColumnName("Height");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<double>("Length")
.HasColumnType("float")
.HasColumnName("Length");
b.Property<int>("NavigationType")
.HasColumnType("int")
.HasColumnName("NavigationType");
b.Property<double>("RadiusWheel")
.HasColumnType("float")
.HasColumnName("RadiusWheel");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.Property<double>("Width")
.HasColumnType("float")
.HasColumnName("Width");
b.HasKey("Id");
b.ToTable("RobotConfig");
});
modelBuilder.Entity("RobotApp.Data.RobotPlcConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<string>("PLCAddress")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("PLCAddress");
b.Property<int>("PLCPort")
.HasColumnType("int")
.HasColumnName("PLCPort");
b.Property<byte>("PLCUnitId")
.HasColumnType("tinyint")
.HasColumnName("PLCUnitId");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.HasKey("Id");
b.ToTable("RobotPlcConfig");
});
modelBuilder.Entity("RobotApp.Data.RobotSafetyConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<double>("SafetySpeedFast")
.HasColumnType("float")
.HasColumnName("SafetySpeedFast");
b.Property<double>("SafetySpeedMedium")
.HasColumnType("float")
.HasColumnName("SafetySpeedMedium");
b.Property<double>("SafetySpeedNormal")
.HasColumnType("float")
.HasColumnName("SafetySpeedNormal");
b.Property<double>("SafetySpeedOptimal")
.HasColumnType("float")
.HasColumnName("SafetySpeedOptimal");
b.Property<double>("SafetySpeedSlow")
.HasColumnType("float")
.HasColumnName("SafetySpeedSlow");
b.Property<double>("SafetySpeedVeryFast")
.HasColumnType("float")
.HasColumnName("SafetySpeedVeryFast");
b.Property<double>("SafetySpeedVerySlow")
.HasColumnType("float")
.HasColumnName("SafetySpeedVerySlow");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.HasKey("Id");
b.ToTable("RobotSafetyConfig");
});
modelBuilder.Entity("RobotApp.Data.RobotSimulationConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<bool>("EnableSimulation")
.HasColumnType("bit")
.HasColumnName("EnableSimulation");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<double>("SimulationAcceleration")
.HasColumnType("float")
.HasColumnName("SimulationAcceleration");
b.Property<double>("SimulationDeceleration")
.HasColumnType("float")
.HasColumnName("SimulationDeceleration");
b.Property<double>("SimulationMaxAngularVelocity")
.HasColumnType("float")
.HasColumnName("SimulationMaxAngularVelocity");
b.Property<double>("SimulationMaxVelocity")
.HasColumnType("float")
.HasColumnName("SimulationMaxVelocity");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.HasKey("Id");
b.ToTable("RobotSimulationConfig");
});
modelBuilder.Entity("RobotApp.Data.RobotVDA5050Config", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<string>("SerialNumber")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("SerialNumber");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.Property<string>("VDA5050CA")
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_CA");
b.Property<string>("VDA5050Cer")
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Cer");
b.Property<bool>("VDA5050EnablePassword")
.HasColumnType("bit")
.HasColumnName("VDA5050_EnablePassword");
b.Property<bool>("VDA5050EnableTls")
.HasColumnType("bit")
.HasColumnName("VDA5050_EnableTls");
b.Property<string>("VDA5050HostServer")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_HostServer");
b.Property<string>("VDA5050Key")
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Key");
b.Property<string>("VDA5050Manufacturer")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Manufacturer");
b.Property<string>("VDA5050Password")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Password");
b.Property<int>("VDA5050Port")
.HasColumnType("int")
.HasColumnName("VDA5050_Port");
b.Property<int>("VDA5050PublishRepeat")
.HasColumnType("int")
.HasColumnName("VDA5050_PublishRepeat");
b.Property<string>("VDA5050TopicPrefix")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_TopicPrefix");
b.Property<string>("VDA5050UserName")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_UserName");
b.Property<string>("VDA5050Version")
.HasMaxLength(20)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Version");
b.HasKey("Id");
b.ToTable("RobotVDA5050Config");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("RobotApp.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,351 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RobotApp.Data.Migrations
{
/// <inheritdoc />
public partial class InitApplicationDb : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RobotConfig",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
NavigationType = table.Column<int>(type: "int", nullable: false),
RadiusWheel = table.Column<double>(type: "float", nullable: false),
Width = table.Column<double>(type: "float", nullable: false),
Length = table.Column<double>(type: "float", nullable: false),
Height = table.Column<double>(type: "float", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
ConfigName = table.Column<string>(type: "nvarchar(64)", maxLength: 100, nullable: true),
Description = table.Column<string>(type: "ntext", maxLength: 500, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RobotConfig", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RobotPlcConfig",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PLCAddress = table.Column<string>(type: "nvarchar(64)", maxLength: 50, nullable: true),
PLCPort = table.Column<int>(type: "int", nullable: false),
PLCUnitId = table.Column<byte>(type: "tinyint", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
ConfigName = table.Column<string>(type: "nvarchar(64)", maxLength: 100, nullable: true),
Description = table.Column<string>(type: "ntext", maxLength: 500, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RobotPlcConfig", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RobotSafetyConfig",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SafetySpeedVerySlow = table.Column<double>(type: "float", nullable: false),
SafetySpeedSlow = table.Column<double>(type: "float", nullable: false),
SafetySpeedNormal = table.Column<double>(type: "float", nullable: false),
SafetySpeedMedium = table.Column<double>(type: "float", nullable: false),
SafetySpeedOptimal = table.Column<double>(type: "float", nullable: false),
SafetySpeedFast = table.Column<double>(type: "float", nullable: false),
SafetySpeedVeryFast = table.Column<double>(type: "float", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
ConfigName = table.Column<string>(type: "nvarchar(64)", maxLength: 100, nullable: true),
Description = table.Column<string>(type: "ntext", maxLength: 500, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RobotSafetyConfig", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RobotSimulationConfig",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
EnableSimulation = table.Column<bool>(type: "bit", nullable: false),
SimulationMaxVelocity = table.Column<double>(type: "float", nullable: false),
SimulationMaxAngularVelocity = table.Column<double>(type: "float", nullable: false),
SimulationAcceleration = table.Column<double>(type: "float", nullable: false),
SimulationDeceleration = table.Column<double>(type: "float", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
ConfigName = table.Column<string>(type: "nvarchar(64)", maxLength: 100, nullable: true),
Description = table.Column<string>(type: "ntext", maxLength: 500, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RobotSimulationConfig", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RobotVDA5050Config",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SerialNumber = table.Column<string>(type: "nvarchar(64)", maxLength: 50, nullable: true),
VDA5050_HostServer = table.Column<string>(type: "nvarchar(64)", maxLength: 100, nullable: true),
VDA5050_Port = table.Column<int>(type: "int", nullable: false),
VDA5050_UserName = table.Column<string>(type: "nvarchar(64)", maxLength: 50, nullable: true),
VDA5050_Password = table.Column<string>(type: "nvarchar(64)", maxLength: 50, nullable: true),
VDA5050_Manufacturer = table.Column<string>(type: "nvarchar(64)", maxLength: 50, nullable: true),
VDA5050_Version = table.Column<string>(type: "nvarchar(64)", maxLength: 20, nullable: true),
VDA5050_TopicPrefix = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
VDA5050_PublishRepeat = table.Column<int>(type: "int", nullable: false),
VDA5050_EnablePassword = table.Column<bool>(type: "bit", nullable: false),
VDA5050_EnableTls = table.Column<bool>(type: "bit", nullable: false),
VDA5050_CA = table.Column<string>(type: "nvarchar(64)", nullable: true),
VDA5050_Cer = table.Column<string>(type: "nvarchar(64)", nullable: true),
VDA5050_Key = table.Column<string>(type: "nvarchar(64)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
ConfigName = table.Column<string>(type: "nvarchar(64)", maxLength: 100, nullable: true),
Description = table.Column<string>(type: "ntext", maxLength: 500, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RobotVDA5050Config", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RoleId = table.Column<string>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", 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: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<string>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", 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: "TEXT", nullable: false),
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", 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: "TEXT", nullable: false),
RoleId = table.Column<string>(type: "TEXT", 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: "TEXT", nullable: false),
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", 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.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
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);
}
/// <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: "RobotConfig");
migrationBuilder.DropTable(
name: "RobotPlcConfig");
migrationBuilder.DropTable(
name: "RobotSafetyConfig");
migrationBuilder.DropTable(
name: "RobotSimulationConfig");
migrationBuilder.DropTable(
name: "RobotVDA5050Config");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}

View File

@@ -7,7 +7,7 @@ using RobotApp.Data;
#nullable disable #nullable disable
namespace RobotApp.Migrations namespace RobotApp.Data.Migrations
{ {
[DbContext(typeof(ApplicationDbContext))] [DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot partial class ApplicationDbContextModelSnapshot : ModelSnapshot
@@ -209,6 +209,320 @@ namespace RobotApp.Migrations
b.ToTable("AspNetUsers", (string)null); b.ToTable("AspNetUsers", (string)null);
}); });
modelBuilder.Entity("RobotApp.Data.RobotConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<double>("Height")
.HasColumnType("float")
.HasColumnName("Height");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<double>("Length")
.HasColumnType("float")
.HasColumnName("Length");
b.Property<int>("NavigationType")
.HasColumnType("int")
.HasColumnName("NavigationType");
b.Property<double>("RadiusWheel")
.HasColumnType("float")
.HasColumnName("RadiusWheel");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.Property<double>("Width")
.HasColumnType("float")
.HasColumnName("Width");
b.HasKey("Id");
b.ToTable("RobotConfig");
});
modelBuilder.Entity("RobotApp.Data.RobotPlcConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<string>("PLCAddress")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("PLCAddress");
b.Property<int>("PLCPort")
.HasColumnType("int")
.HasColumnName("PLCPort");
b.Property<byte>("PLCUnitId")
.HasColumnType("tinyint")
.HasColumnName("PLCUnitId");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.HasKey("Id");
b.ToTable("RobotPlcConfig");
});
modelBuilder.Entity("RobotApp.Data.RobotSafetyConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<double>("SafetySpeedFast")
.HasColumnType("float")
.HasColumnName("SafetySpeedFast");
b.Property<double>("SafetySpeedMedium")
.HasColumnType("float")
.HasColumnName("SafetySpeedMedium");
b.Property<double>("SafetySpeedNormal")
.HasColumnType("float")
.HasColumnName("SafetySpeedNormal");
b.Property<double>("SafetySpeedOptimal")
.HasColumnType("float")
.HasColumnName("SafetySpeedOptimal");
b.Property<double>("SafetySpeedSlow")
.HasColumnType("float")
.HasColumnName("SafetySpeedSlow");
b.Property<double>("SafetySpeedVeryFast")
.HasColumnType("float")
.HasColumnName("SafetySpeedVeryFast");
b.Property<double>("SafetySpeedVerySlow")
.HasColumnType("float")
.HasColumnName("SafetySpeedVerySlow");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.HasKey("Id");
b.ToTable("RobotSafetyConfig");
});
modelBuilder.Entity("RobotApp.Data.RobotSimulationConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<bool>("EnableSimulation")
.HasColumnType("bit")
.HasColumnName("EnableSimulation");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<double>("SimulationAcceleration")
.HasColumnType("float")
.HasColumnName("SimulationAcceleration");
b.Property<double>("SimulationDeceleration")
.HasColumnType("float")
.HasColumnName("SimulationDeceleration");
b.Property<double>("SimulationMaxAngularVelocity")
.HasColumnType("float")
.HasColumnName("SimulationMaxAngularVelocity");
b.Property<double>("SimulationMaxVelocity")
.HasColumnType("float")
.HasColumnName("SimulationMaxVelocity");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.HasKey("Id");
b.ToTable("RobotSimulationConfig");
});
modelBuilder.Entity("RobotApp.Data.RobotVDA5050Config", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("ConfigName")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("ConfigName");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("ntext")
.HasColumnName("Description");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<string>("SerialNumber")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("SerialNumber");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("UpdatedAt");
b.Property<string>("VDA5050CA")
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_CA");
b.Property<string>("VDA5050Cer")
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Cer");
b.Property<bool>("VDA5050EnablePassword")
.HasColumnType("bit")
.HasColumnName("VDA5050_EnablePassword");
b.Property<bool>("VDA5050EnableTls")
.HasColumnType("bit")
.HasColumnName("VDA5050_EnableTls");
b.Property<string>("VDA5050HostServer")
.HasMaxLength(100)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_HostServer");
b.Property<string>("VDA5050Key")
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Key");
b.Property<string>("VDA5050Manufacturer")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Manufacturer");
b.Property<string>("VDA5050Password")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Password");
b.Property<int>("VDA5050Port")
.HasColumnType("int")
.HasColumnName("VDA5050_Port");
b.Property<int>("VDA5050PublishRepeat")
.HasColumnType("int")
.HasColumnName("VDA5050_PublishRepeat");
b.Property<string>("VDA5050TopicPrefix")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_TopicPrefix");
b.Property<string>("VDA5050UserName")
.HasMaxLength(50)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_UserName");
b.Property<string>("VDA5050Version")
.HasMaxLength(20)
.HasColumnType("nvarchar(64)")
.HasColumnName("VDA5050_Version");
b.HasKey("Id");
b.ToTable("RobotVDA5050Config");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.HasOne("RobotApp.Data.ApplicationRole", null) b.HasOne("RobotApp.Data.ApplicationRole", null)

View File

@@ -0,0 +1,49 @@
using RobotApp.Common.Shares.Enums;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RobotApp.Data;
#nullable disable
[Table("RobotConfig")]
public class RobotConfig
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("NavigationType", TypeName = "int")]
public NavigationType NavigationType { get; set; }
[Column("RadiusWheel", TypeName = "float")]
public double RadiusWheel { get; set; }
[Column("Width", TypeName = "float")]
public double Width { get; set; }
[Column("Length", TypeName = "float")]
public double Length { get; set; }
[Column("Height", TypeName = "float")]
public double Height { get; set; }
[Column("CreatedAt", TypeName = "datetime2")]
public DateTime CreatedAt { get; set; }
[Column("UpdatedAt", TypeName = "datetime2")]
public DateTime UpdatedAt { get; set; }
[Column("IsActive", TypeName = "bit")]
public bool IsActive { get; set; }
[Column("ConfigName", TypeName = "nvarchar(64)")]
[MaxLength(100)]
public string ConfigName { get; set; }
[Column("Description", TypeName = "ntext")]
[MaxLength(500)]
public string Description { get; set; }
}

View File

@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RobotApp.Data;
#nullable disable
[Table("RobotPlcConfig")]
public class RobotPlcConfig
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("PLCAddress", TypeName = "nvarchar(64)")]
[MaxLength(50)]
public string PLCAddress { get; set; }
[Column("PLCPort", TypeName = "int")]
public int PLCPort { get; set; }
[Column("PLCUnitId", TypeName = "tinyint")]
public byte PLCUnitId { get; set; }
[Column("CreatedAt", TypeName = "datetime2")]
public DateTime CreatedAt { get; set; }
[Column("UpdatedAt", TypeName = "datetime2")]
public DateTime UpdatedAt { get; set; }
[Column("IsActive", TypeName = "bit")]
public bool IsActive { get; set; }
[Column("ConfigName", TypeName = "nvarchar(64)")]
[MaxLength(100)]
public string ConfigName { get; set; }
[Column("Description", TypeName = "ntext")]
[MaxLength(500)]
public string Description { get; set; }
}

View File

@@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RobotApp.Data;
#nullable disable
[Table("RobotSafetyConfig")]
public class RobotSafetyConfig
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("SafetySpeedVerySlow", TypeName = "float")]
public double SafetySpeedVerySlow { get; set; }
[Column("SafetySpeedSlow", TypeName = "float")]
public double SafetySpeedSlow { get; set; }
[Column("SafetySpeedNormal", TypeName = "float")]
public double SafetySpeedNormal { get; set; }
[Column("SafetySpeedMedium", TypeName = "float")]
public double SafetySpeedMedium { get; set; }
[Column("SafetySpeedOptimal", TypeName = "float")]
public double SafetySpeedOptimal { get; set; }
[Column("SafetySpeedFast", TypeName = "float")]
public double SafetySpeedFast { get; set; }
[Column("SafetySpeedVeryFast", TypeName = "float")]
public double SafetySpeedVeryFast { get; set; }
[Column("CreatedAt", TypeName = "datetime2")]
public DateTime CreatedAt { get; set; }
[Column("UpdatedAt", TypeName = "datetime2")]
public DateTime UpdatedAt { get; set; }
[Column("IsActive", TypeName = "bit")]
public bool IsActive { get; set; }
[Column("ConfigName", TypeName = "nvarchar(64)")]
[MaxLength(100)]
public string ConfigName { get; set; }
[Column("Description", TypeName = "ntext")]
[MaxLength(500)]
public string Description { get; set; }
}

View File

@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RobotApp.Data;
#nullable disable
[Table("RobotSimulationConfig")]
public class RobotSimulationConfig
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("EnableSimulation", TypeName = "bit")]
public bool EnableSimulation { get; set; }
[Column("SimulationMaxVelocity", TypeName = "float")]
public double SimulationMaxVelocity { get; set; }
[Column("SimulationMaxAngularVelocity", TypeName = "float")]
public double SimulationMaxAngularVelocity { get; set; }
[Column("SimulationAcceleration", TypeName = "float")]
public double SimulationAcceleration { get; set; }
[Column("SimulationDeceleration", TypeName = "float")]
public double SimulationDeceleration { get; set; }
[Column("CreatedAt", TypeName = "datetime2")]
public DateTime CreatedAt { get; set; }
[Column("UpdatedAt", TypeName = "datetime2")]
public DateTime UpdatedAt { get; set; }
[Column("IsActive", TypeName = "bit")]
public bool IsActive { get; set; }
[Column("ConfigName", TypeName = "nvarchar(64)")]
[MaxLength(100)]
public string ConfigName { get; set; }
[Column("Description", TypeName = "ntext")]
[MaxLength(500)]
public string Description { get; set; }
}

View File

@@ -0,0 +1,83 @@
using RobotApp.Common.Shares.Enums;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RobotApp.Data;
#nullable disable
[Table("RobotVDA5050Config")]
public class RobotVDA5050Config
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("SerialNumber", TypeName = "nvarchar(64)")]
[MaxLength(50)]
public string SerialNumber { get; set; }
[Column("VDA5050_HostServer", TypeName = "nvarchar(64)")]
[MaxLength(100)]
public string VDA5050HostServer { get; set; }
[Column("VDA5050_Port", TypeName = "int")]
public int VDA5050Port { get; set; }
[Column("VDA5050_UserName", TypeName = "nvarchar(64)")]
[MaxLength(50)]
public string VDA5050UserName { get; set; }
[Column("VDA5050_Password", TypeName = "nvarchar(64)")]
[MaxLength(50)]
public string VDA5050Password { get; set; }
[Column("VDA5050_Manufacturer", TypeName = "nvarchar(64)")]
[MaxLength(50)]
public string VDA5050Manufacturer { get; set; }
[Column("VDA5050_Version", TypeName = "nvarchar(64)")]
[MaxLength(20)]
public string VDA5050Version { get; set; }
[Column("VDA5050_TopicPrefix", TypeName = "nvarchar(64)")]
[MaxLength(64)]
public string VDA5050TopicPrefix { get; set; }
[Column("VDA5050_PublishRepeat", TypeName = "int")]
public int VDA5050PublishRepeat { get; set; }
[Column("VDA5050_EnablePassword", TypeName = "bit")]
public bool VDA5050EnablePassword { get; set; }
[Column("VDA5050_EnableTls", TypeName = "bit")]
public bool VDA5050EnableTls { get; set; }
[Column("VDA5050_CA", TypeName = "nvarchar(64)")]
public string VDA5050CA { get; set; }
[Column("VDA5050_Cer", TypeName = "nvarchar(64)")]
public string VDA5050Cer { get; set; }
[Column("VDA5050_Key", TypeName = "nvarchar(64)")]
public string VDA5050Key { get; set; }
[Column("CreatedAt", TypeName = "datetime2")]
public DateTime CreatedAt { get; set; }
[Column("UpdatedAt", TypeName = "datetime2")]
public DateTime UpdatedAt { get; set; }
[Column("IsActive", TypeName = "bit")]
public bool IsActive { get; set; }
[Column("ConfigName", TypeName = "nvarchar(64)")]
[MaxLength(100)]
public string ConfigName { get; set; }
[Column("Description", TypeName = "ntext")]
[MaxLength(500)]
public string Description { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More