diff --git a/RobotApp.Common.Shares/Dtos/RobotConfigDto.cs b/RobotApp.Common.Shares/Dtos/RobotConfigDto.cs new file mode 100644 index 0000000..eafee69 --- /dev/null +++ b/RobotApp.Common.Shares/Dtos/RobotConfigDto.cs @@ -0,0 +1,20 @@ +using RobotApp.Common.Shares.Enums; + +namespace RobotApp.Common.Shares.Dtos; + +#nullable disable + +public record RobotConfigDto +{ + public Guid Id { 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; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public bool IsActive { get; set; } = true; + public string ConfigName { get; set; } + public string Description { get; set; } +} diff --git a/RobotApp.Common.Shares/Dtos/RobotPlcConfigDto.cs b/RobotApp.Common.Shares/Dtos/RobotPlcConfigDto.cs new file mode 100644 index 0000000..d6a2648 --- /dev/null +++ b/RobotApp.Common.Shares/Dtos/RobotPlcConfigDto.cs @@ -0,0 +1,16 @@ +namespace RobotApp.Common.Shares.Dtos; + +#nullable disable + +public record RobotPlcConfigDto +{ + public Guid Id { get; set; } + public string PLCAddress { get; set; } + public int PLCPort { get; set; } + public byte PLCUnitId { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public bool IsActive { get; set; } = true; + public string ConfigName { get; set; } + public string Description { get; set; } +} diff --git a/RobotApp.Common.Shares/Dtos/RobotSafetyConfig.cs b/RobotApp.Common.Shares/Dtos/RobotSafetyConfig.cs new file mode 100644 index 0000000..9837c18 --- /dev/null +++ b/RobotApp.Common.Shares/Dtos/RobotSafetyConfig.cs @@ -0,0 +1,20 @@ +namespace RobotApp.Common.Shares.Dtos; + +#nullable disable + +public record RobotSafetyConfig +{ + public Guid Id { get; set; } + 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 DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public bool IsActive { get; set; } = true; + public string ConfigName { get; set; } + public string Description { get; set; } +} diff --git a/RobotApp.Common.Shares/Dtos/RobotSimulationConfigDto.cs b/RobotApp.Common.Shares/Dtos/RobotSimulationConfigDto.cs new file mode 100644 index 0000000..7630685 --- /dev/null +++ b/RobotApp.Common.Shares/Dtos/RobotSimulationConfigDto.cs @@ -0,0 +1,18 @@ +namespace RobotApp.Common.Shares.Dtos; + +#nullable disable + +public record RobotSimulationConfigDto +{ + public Guid Id { get; set; } + 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 DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public bool IsActive { get; set; } = true; + public string ConfigName { get; set; } + public string Description { get; set; } +} diff --git a/RobotApp.Common.Shares/Dtos/RobotVDA5050ConfigDto.cs b/RobotApp.Common.Shares/Dtos/RobotVDA5050ConfigDto.cs new file mode 100644 index 0000000..942981a --- /dev/null +++ b/RobotApp.Common.Shares/Dtos/RobotVDA5050ConfigDto.cs @@ -0,0 +1,23 @@ +namespace RobotApp.Common.Shares.Dtos; + +#nullable disable + +public record RobotVDA5050ConfigDto +{ + public Guid Id { get; set; } + public string SerialNumber { get; set; } + public string VDA5050HostServer { get; set; } + public int VDA5050Port { get; set; } = 1883; + public string VDA5050UserName { get; set; } + public string VDA5050Password { get; set; } + public string VDA5050Manufacturer { get; set; } + public string VDA5050Version { get; set; } + public int VDA5050PublishRepeat { get; set; } + public bool VDA5050EnablePassword { get; set; } + public bool VDA5050EnableTls { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public bool IsActive { get; set; } = true; + public string ConfigName { get; set; } + public string Description { get; set; } +} diff --git a/RobotApp.VDA5050/VDA5050Setting.cs b/RobotApp.VDA5050/VDA5050Setting.cs index 95cac01..9473c9b 100644 --- a/RobotApp.VDA5050/VDA5050Setting.cs +++ b/RobotApp.VDA5050/VDA5050Setting.cs @@ -1,5 +1,6 @@ namespace RobotApp.VDA5050; + public class VDA5050Setting { public string HostServer { get; set; } = string.Empty; @@ -7,6 +8,8 @@ public class VDA5050Setting public string UserName { get; set; } = "robotics"; public string Password { get; set; } = "robotics"; 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 bool EnablePassword { get; set; } = false; + public bool EnableTls { get; set; } = false; } diff --git a/RobotApp/Controllers/RobotConfigsController.cs b/RobotApp/Controllers/RobotConfigsController.cs new file mode 100644 index 0000000..ad17b1b --- /dev/null +++ b/RobotApp/Controllers/RobotConfigsController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace RobotApp.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class RobotConfigsController(Services.Logger Logger) : ControllerBase +{ + [HttpGet] + [Route("plc")] + public void GetPLCConfig() + { + } +} diff --git a/RobotApp/Data/ApplicationDbContext.cs b/RobotApp/Data/ApplicationDbContext.cs index e1bc9ad..0530c5c 100644 --- a/RobotApp/Data/ApplicationDbContext.cs +++ b/RobotApp/Data/ApplicationDbContext.cs @@ -1,5 +1,8 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using RobotApp.VDA5050.Order; +using RobotApp.VDA5050.State; +using static MudBlazor.CategoryTypes; namespace RobotApp.Data { @@ -8,5 +11,10 @@ namespace RobotApp.Data public ApplicationDbContext(DbContextOptions options) : base(options) { } + + public DbSet RobotConfigs { get; private set; } + public DbSet RobotSimulationConfigs { get; private set; } + public DbSet RobotPlcConfigs { get; private set; } + public DbSet RobotVDA5050Configs { get; private set; } } } diff --git a/RobotApp/Data/ApplicationDbExtensions.cs b/RobotApp/Data/ApplicationDbExtensions.cs index 7b4383d..f89033c 100644 --- a/RobotApp/Data/ApplicationDbExtensions.cs +++ b/RobotApp/Data/ApplicationDbExtensions.cs @@ -17,6 +17,7 @@ public static class ApplicationDbExtensions await scope.ServiceProvider.SeedRolesAsync(); await scope.ServiceProvider.SeedUsersAsync(); + await scope.ServiceProvider.SeedConfigAsync(); } private static async Task SeedRolesAsync(this IServiceProvider serviceProvider) @@ -31,6 +32,15 @@ public static class ApplicationDbExtensions NormalizedName = "ADMINISTRATOR", }); } + + if (!await roleManager.RoleExistsAsync("Distributor")) + { + await roleManager.CreateAsync(new ApplicationRole() + { + Name = "Distributor", + NormalizedName = "DISTRIBUTOR", + }); + } } private static async Task SeedUsersAsync(this IServiceProvider serviceProvider) @@ -50,5 +60,108 @@ public static class ApplicationDbExtensions await userManager.CreateAsync(admin, "robotics"); 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(); + + 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 = false, + 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", + IsActive = true, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now, + }; + + appDb.RobotVDA5050Configs.Add(defaultConfig); + await appDb.SaveChangesAsync(); + } } } diff --git a/RobotApp/Data/Migrations/20251028102815_InitConfigDb.Designer.cs b/RobotApp/Data/Migrations/20251028102815_InitConfigDb.Designer.cs new file mode 100644 index 0000000..62ad2b7 --- /dev/null +++ b/RobotApp/Data/Migrations/20251028102815_InitConfigDb.Designer.cs @@ -0,0 +1,503 @@ +// +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("20251028102815_InitConfigDb")] + partial class InitConfigDb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RobotApp.Data.ApplicationRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ConfigName") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("ConfigName"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("Height") + .HasColumnType("float") + .HasColumnName("Height"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("Length") + .HasColumnType("float") + .HasColumnName("Length"); + + b.Property("NavigationType") + .HasColumnType("int") + .HasColumnName("NavigationType"); + + b.Property("RadiusWheel") + .HasColumnType("float") + .HasColumnName("RadiusWheel"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("UpdatedAt"); + + b.Property("Width") + .HasColumnType("float") + .HasColumnName("Width"); + + b.HasKey("Id"); + + b.ToTable("RobotConfig"); + }); + + modelBuilder.Entity("RobotApp.Data.RobotPlcConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ConfigName") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("ConfigName"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("PLCAddress") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("PLCAddress"); + + b.Property("PLCPort") + .HasColumnType("int") + .HasColumnName("PLCPort"); + + b.Property("PLCUnitId") + .HasColumnType("tinyint") + .HasColumnName("PLCUnitId"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("UpdatedAt"); + + b.HasKey("Id"); + + b.ToTable("RobotPlcConfig"); + }); + + modelBuilder.Entity("RobotApp.Data.RobotSimulationConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ConfigName") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("ConfigName"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("EnableSimulation") + .HasColumnType("bit") + .HasColumnName("EnableSimulation"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("SimulationAcceleration") + .HasColumnType("float") + .HasColumnName("SimulationAcceleration"); + + b.Property("SimulationDeceleration") + .HasColumnType("float") + .HasColumnName("SimulationDeceleration"); + + b.Property("SimulationMaxAngularVelocity") + .HasColumnType("float") + .HasColumnName("SimulationMaxAngularVelocity"); + + b.Property("SimulationMaxVelocity") + .HasColumnType("float") + .HasColumnName("SimulationMaxVelocity"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("UpdatedAt"); + + b.HasKey("Id"); + + b.ToTable("RobotSimulationConfig"); + }); + + modelBuilder.Entity("RobotApp.Data.RobotVDA5050Config", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ConfigName") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("ConfigName"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("SerialNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("SerialNumber"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("UpdatedAt"); + + b.Property("VDA5050EnablePassword") + .HasColumnType("bit") + .HasColumnName("VDA5050_EnablePassword"); + + b.Property("VDA5050EnableTls") + .HasColumnType("bit") + .HasColumnName("VDA5050_EnableTls"); + + b.Property("VDA5050HostServer") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_HostServer"); + + b.Property("VDA5050Manufacturer") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_Manufacturer"); + + b.Property("VDA5050Password") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_Password"); + + b.Property("VDA5050Port") + .HasColumnType("int") + .HasColumnName("VDA5050_Port"); + + b.Property("VDA5050PublishRepeat") + .HasColumnType("int") + .HasColumnName("VDA5050_PublishRepeat"); + + b.Property("VDA5050UserName") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_UserName"); + + b.Property("VDA5050Version") + .HasMaxLength(20) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_Version"); + + b.HasKey("Id"); + + b.ToTable("RobotVDA5050Config"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RobotApp.Data.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", 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", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotApp/Data/Migrations/20251028102815_InitConfigDb.cs b/RobotApp/Data/Migrations/20251028102815_InitConfigDb.cs new file mode 100644 index 0000000..924cfa1 --- /dev/null +++ b/RobotApp/Data/Migrations/20251028102815_InitConfigDb.cs @@ -0,0 +1,118 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotApp.Data.Migrations +{ + /// + public partial class InitConfigDb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RobotConfig", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + NavigationType = table.Column(type: "int", nullable: false), + RadiusWheel = table.Column(type: "float", nullable: false), + Width = table.Column(type: "float", nullable: false), + Length = table.Column(type: "float", nullable: false), + Height = table.Column(type: "float", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsActive = table.Column(type: "bit", nullable: false), + ConfigName = table.Column(type: "nvarchar(64)", maxLength: 100, nullable: true), + Description = table.Column(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(type: "uniqueidentifier", nullable: false), + PLCAddress = table.Column(type: "nvarchar(64)", maxLength: 50, nullable: true), + PLCPort = table.Column(type: "int", nullable: false), + PLCUnitId = table.Column(type: "tinyint", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsActive = table.Column(type: "bit", nullable: false), + ConfigName = table.Column(type: "nvarchar(64)", maxLength: 100, nullable: true), + Description = table.Column(type: "ntext", maxLength: 500, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RobotPlcConfig", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "RobotSimulationConfig", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + EnableSimulation = table.Column(type: "bit", nullable: false), + SimulationMaxVelocity = table.Column(type: "float", nullable: false), + SimulationMaxAngularVelocity = table.Column(type: "float", nullable: false), + SimulationAcceleration = table.Column(type: "float", nullable: false), + SimulationDeceleration = table.Column(type: "float", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsActive = table.Column(type: "bit", nullable: false), + ConfigName = table.Column(type: "nvarchar(64)", maxLength: 100, nullable: true), + Description = table.Column(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(type: "uniqueidentifier", nullable: false), + SerialNumber = table.Column(type: "nvarchar(64)", maxLength: 50, nullable: true), + VDA5050_HostServer = table.Column(type: "nvarchar(64)", maxLength: 100, nullable: true), + VDA5050_Port = table.Column(type: "int", nullable: false), + VDA5050_UserName = table.Column(type: "nvarchar(64)", maxLength: 50, nullable: true), + VDA5050_Password = table.Column(type: "nvarchar(64)", maxLength: 50, nullable: true), + VDA5050_Manufacturer = table.Column(type: "nvarchar(64)", maxLength: 50, nullable: true), + VDA5050_Version = table.Column(type: "nvarchar(64)", maxLength: 20, nullable: true), + VDA5050_PublishRepeat = table.Column(type: "int", nullable: false), + VDA5050_EnablePassword = table.Column(type: "bit", nullable: false), + VDA5050_EnableTls = table.Column(type: "bit", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsActive = table.Column(type: "bit", nullable: false), + ConfigName = table.Column(type: "nvarchar(64)", maxLength: 100, nullable: true), + Description = table.Column(type: "ntext", maxLength: 500, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RobotVDA5050Config", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RobotConfig"); + + migrationBuilder.DropTable( + name: "RobotPlcConfig"); + + migrationBuilder.DropTable( + name: "RobotSimulationConfig"); + + migrationBuilder.DropTable( + name: "RobotVDA5050Config"); + } + } +} diff --git a/RobotApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/RobotApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 3f45751..1dcd830 100644 --- a/RobotApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/RobotApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -209,6 +209,241 @@ namespace RobotApp.Migrations b.ToTable("AspNetUsers", (string)null); }); + modelBuilder.Entity("RobotApp.Data.RobotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ConfigName") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("ConfigName"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("Height") + .HasColumnType("float") + .HasColumnName("Height"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("Length") + .HasColumnType("float") + .HasColumnName("Length"); + + b.Property("NavigationType") + .HasColumnType("int") + .HasColumnName("NavigationType"); + + b.Property("RadiusWheel") + .HasColumnType("float") + .HasColumnName("RadiusWheel"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("UpdatedAt"); + + b.Property("Width") + .HasColumnType("float") + .HasColumnName("Width"); + + b.HasKey("Id"); + + b.ToTable("RobotConfig"); + }); + + modelBuilder.Entity("RobotApp.Data.RobotPlcConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ConfigName") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("ConfigName"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("PLCAddress") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("PLCAddress"); + + b.Property("PLCPort") + .HasColumnType("int") + .HasColumnName("PLCPort"); + + b.Property("PLCUnitId") + .HasColumnType("tinyint") + .HasColumnName("PLCUnitId"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("UpdatedAt"); + + b.HasKey("Id"); + + b.ToTable("RobotPlcConfig"); + }); + + modelBuilder.Entity("RobotApp.Data.RobotSimulationConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ConfigName") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("ConfigName"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("EnableSimulation") + .HasColumnType("bit") + .HasColumnName("EnableSimulation"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("SimulationAcceleration") + .HasColumnType("float") + .HasColumnName("SimulationAcceleration"); + + b.Property("SimulationDeceleration") + .HasColumnType("float") + .HasColumnName("SimulationDeceleration"); + + b.Property("SimulationMaxAngularVelocity") + .HasColumnType("float") + .HasColumnName("SimulationMaxAngularVelocity"); + + b.Property("SimulationMaxVelocity") + .HasColumnType("float") + .HasColumnName("SimulationMaxVelocity"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("UpdatedAt"); + + b.HasKey("Id"); + + b.ToTable("RobotSimulationConfig"); + }); + + modelBuilder.Entity("RobotApp.Data.RobotVDA5050Config", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ConfigName") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("ConfigName"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("SerialNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("SerialNumber"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("UpdatedAt"); + + b.Property("VDA5050EnablePassword") + .HasColumnType("bit") + .HasColumnName("VDA5050_EnablePassword"); + + b.Property("VDA5050EnableTls") + .HasColumnType("bit") + .HasColumnName("VDA5050_EnableTls"); + + b.Property("VDA5050HostServer") + .HasMaxLength(100) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_HostServer"); + + b.Property("VDA5050Manufacturer") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_Manufacturer"); + + b.Property("VDA5050Password") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_Password"); + + b.Property("VDA5050Port") + .HasColumnType("int") + .HasColumnName("VDA5050_Port"); + + b.Property("VDA5050PublishRepeat") + .HasColumnType("int") + .HasColumnName("VDA5050_PublishRepeat"); + + b.Property("VDA5050UserName") + .HasMaxLength(50) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_UserName"); + + b.Property("VDA5050Version") + .HasMaxLength(20) + .HasColumnType("nvarchar(64)") + .HasColumnName("VDA5050_Version"); + + b.HasKey("Id"); + + b.ToTable("RobotVDA5050Config"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("RobotApp.Data.ApplicationRole", null) diff --git a/RobotApp/Data/RobotConfig.cs b/RobotApp/Data/RobotConfig.cs new file mode 100644 index 0000000..acb9a20 --- /dev/null +++ b/RobotApp/Data/RobotConfig.cs @@ -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; } = true; + + [Column("ConfigName", TypeName = "nvarchar(64)")] + [MaxLength(100)] + public string ConfigName { get; set; } + + [Column("Description", TypeName = "ntext")] + [MaxLength(500)] + public string Description { get; set; } +} diff --git a/RobotApp/Data/RobotPlcConfig.cs b/RobotApp/Data/RobotPlcConfig.cs new file mode 100644 index 0000000..5fe4b2a --- /dev/null +++ b/RobotApp/Data/RobotPlcConfig.cs @@ -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; } = true; + + [Column("ConfigName", TypeName = "nvarchar(64)")] + [MaxLength(100)] + public string ConfigName { get; set; } + + [Column("Description", TypeName = "ntext")] + [MaxLength(500)] + public string Description { get; set; } +} diff --git a/RobotApp/Data/RobotSafetyConfig.cs b/RobotApp/Data/RobotSafetyConfig.cs new file mode 100644 index 0000000..53f22db --- /dev/null +++ b/RobotApp/Data/RobotSafetyConfig.cs @@ -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; } = true; + + [Column("ConfigName", TypeName = "nvarchar(64)")] + [MaxLength(100)] + public string ConfigName { get; set; } + + [Column("Description", TypeName = "ntext")] + [MaxLength(500)] + public string Description { get; set; } +} diff --git a/RobotApp/Data/RobotSimulationConfig.cs b/RobotApp/Data/RobotSimulationConfig.cs new file mode 100644 index 0000000..f41476c --- /dev/null +++ b/RobotApp/Data/RobotSimulationConfig.cs @@ -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; } = true; + + [Column("ConfigName", TypeName = "nvarchar(64)")] + [MaxLength(100)] + public string ConfigName { get; set; } + + [Column("Description", TypeName = "ntext")] + [MaxLength(500)] + public string Description { get; set; } +} diff --git a/RobotApp/Data/RobotVDA5050Config.cs b/RobotApp/Data/RobotVDA5050Config.cs new file mode 100644 index 0000000..b3665fb --- /dev/null +++ b/RobotApp/Data/RobotVDA5050Config.cs @@ -0,0 +1,70 @@ +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; } = 1883; + + [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_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("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; } = true; + + [Column("ConfigName", TypeName = "nvarchar(64)")] + [MaxLength(100)] + public string ConfigName { get; set; } + + [Column("Description", TypeName = "ntext")] + [MaxLength(500)] + public string Description { get; set; } +} diff --git a/RobotApp/Program.cs b/RobotApp/Program.cs index 358ace5..a7e797a 100644 --- a/RobotApp/Program.cs +++ b/RobotApp/Program.cs @@ -6,7 +6,6 @@ using RobotApp.Components; using RobotApp.Components.Account; using RobotApp.Data; using RobotApp.Services; -using RobotApp.Services.Robot; using RobotApp.Services.Robot.Simulation; var builder = WebApplication.CreateBuilder(args); diff --git a/RobotApp/Services/MQTTClient.cs b/RobotApp/Services/MQTTClient.cs index 20aba0f..e4d4d65 100644 --- a/RobotApp/Services/MQTTClient.cs +++ b/RobotApp/Services/MQTTClient.cs @@ -2,6 +2,8 @@ using MQTTnet.Protocol; using RobotApp.Common.Shares; using RobotApp.VDA5050; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Text; namespace RobotApp.Services; @@ -9,28 +11,31 @@ namespace RobotApp.Services; public class MQTTClient : IAsyncDisposable { private readonly MqttClientFactory MqttClientFactory; - private readonly MqttClientOptions MqttClientOptions; + private MqttClientOptions MqttClientOptions; private readonly MqttClientSubscribeOptions MqttClientSubscribeOptions; private IMqttClient? MqttClient; private readonly Logger Logger; private readonly VDA5050Setting VDA5050Setting; - private bool IsReconnecing; + private readonly string ClientId; + private readonly SemaphoreSlim ReconnectionSemaphore = new(1, 1); + private volatile bool IsDisposed; public event Action? OrderChanged; public event Action? InstanceActionsChanged; - public bool IsConnected => !IsReconnecing && MqttClient is not null && MqttClient.IsConnected; + public bool IsConnected => !IsDisposed && MqttClient is not null && MqttClient.IsConnected; public MQTTClient(string clientId, VDA5050Setting setting, Logger logger) { VDA5050Setting = setting; + ClientId = clientId; Logger = logger; + MqttClientFactory = new MqttClientFactory(); MqttClientOptions = MqttClientFactory.CreateClientOptionsBuilder() - .WithTcpServer(setting.HostServer, setting.Port) - .WithCredentials(setting.UserName, setting.Password) - .WithClientId(clientId) + .WithTcpServer(VDA5050Setting.HostServer, VDA5050Setting.Port) + .WithClientId(ClientId) .WithCleanSession(true) .Build(); MqttClientSubscribeOptions = MqttClientFactory.CreateSubscribeOptionsBuilder() @@ -39,93 +44,231 @@ public class MQTTClient : IAsyncDisposable .Build(); } - public async Task ConnectAsync(CancellationToken cancellationToken = default) + private async Task OnDisconnected(MqttClientDisconnectedEventArgs args) { - MqttClient = MqttClientFactory.CreateMqttClient(); - MqttClient.DisconnectedAsync += async delegate (MqttClientDisconnectedEventArgs args) - { - if (args.ClientWasConnected && !IsReconnecing) - { - IsReconnecing = true; - Logger.Warning("Mất kết nối tới broker, đang cố gắng kết nối lại..."); - if (MqttClient.IsConnected) await MqttClient.DisconnectAsync(); - MqttClient.Dispose(); + if (IsDisposed || !args.ClientWasConnected) return; - await ConnectAsync(); - await SubscribeAsync(); - IsReconnecing = false; - } - }; - while (!cancellationToken.IsCancellationRequested) + if (!await ReconnectionSemaphore.WaitAsync(0)) + { + Logger.Info("Reconnection đã đang được thực hiện bởi thread khác"); + return; + } + + try + { + Logger.Warning("Mất kết nối tới broker, đang cố gắng kết nối lại..."); + + await CleanupCurrentClient(); + + await ReconnectWithRetry(); + } + catch (Exception ex) + { + Logger.Error($"Lỗi trong quá trình reconnection: {ex.Message}"); + } + finally + { + ReconnectionSemaphore.Release(); + } + } + private async Task CleanupCurrentClient() + { + if (MqttClient is not null) { try { - var connection = await MqttClient.ConnectAsync(MqttClientOptions, cancellationToken); - if (connection.ResultCode != MqttClientConnectResultCode.Success || !MqttClient.IsConnected) - Logger.Warning($"Không thể kết nối tới broker do: {connection.ReasonString}"); - else + MqttClient.DisconnectedAsync -= OnDisconnected; + MqttClient.ApplicationMessageReceivedAsync -= OnMessageReceived; + + if (MqttClient.IsConnected) { - Logger.Info("Kết nối tới broker thành công"); - break; + await MqttClient.DisconnectAsync(); } } catch (Exception ex) { - Logger.Error($"Lỗi khi tạo MQTT client: {ex.Message}"); + Logger.Warning($"Lỗi khi cleanup client: {ex.Message}"); + } + finally + { + MqttClient.Dispose(); + MqttClient = null; } - await Task.Delay(3000, cancellationToken); } } + private async Task ReconnectWithRetry() + { + const int maxRetries = 5; + const int retryDelayMs = 3000; + + for (int attempt = 1; attempt <= maxRetries && !IsDisposed; attempt++) + { + try + { + Logger.Info($"Thử reconnect lần {attempt}/{maxRetries}"); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await ConnectAsync(cts.Token); + + if (IsConnected) + { + await SubscribeAsync(cts.Token); + Logger.Info("Reconnection thành công"); + return; + } + } + catch (OperationCanceledException) + { + Logger.Warning($"Reconnect attempt {attempt} bị timeout"); + } + catch (Exception ex) + { + Logger.Error($"Reconnect attempt {attempt} thất bại: {ex.Message}"); + } + + if (attempt < maxRetries && !IsDisposed) + { + await Task.Delay(retryDelayMs * attempt); + } + } + + Logger.Error("Không thể reconnect sau tất cả các attempts"); + } + + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + if (!IsDisposed) + { + BuildMqttClientOptions(VDA5050Setting.EnablePassword, VDA5050Setting.EnableTls); + await CleanupCurrentClient(); + + MqttClient = MqttClientFactory.CreateMqttClient(); + MqttClient.DisconnectedAsync += OnDisconnected; + while (!cancellationToken.IsCancellationRequested) + { + try + { + var connection = await MqttClient.ConnectAsync(MqttClientOptions, cancellationToken); + if (connection.ResultCode != MqttClientConnectResultCode.Success || !MqttClient.IsConnected) + Logger.Warning($"Không thể kết nối tới broker do: {connection.ReasonString}"); + else + { + Logger.Info("Kết nối tới broker thành công"); + break; + } + } + catch (Exception ex) + { + Logger.Error($"Lỗi khi tạo MQTT client: {ex.Message}"); + } + await Task.Delay(3000, cancellationToken); + } + } + else throw new ObjectDisposedException(nameof(MQTTClient)); + } + + private void BuildMqttClientOptions(bool enablePassword, bool enableTls) + { + var builder = MqttClientFactory.CreateClientOptionsBuilder() + .WithTcpServer(VDA5050Setting.HostServer, VDA5050Setting.Port) + .WithClientId(ClientId) + .WithCleanSession(true); + if (enablePassword) + { + builder.WithCredentials(VDA5050Setting.UserName, VDA5050Setting.Password); + } + + if (enableTls) + { + var tlsOptions = new MqttClientTlsOptionsBuilder() + .UseTls(true) + .WithSslProtocols(System.Security.Authentication.SslProtocols.Tls12) + .WithCertificateValidationHandler(context => + { + if (context.SslPolicyErrors != SslPolicyErrors.None) + { + Logger.Warning($"[TLS] Lỗi: {context.SslPolicyErrors}"); + return false; + } + return true; + }) + .Build(); + builder.WithTlsOptions(tlsOptions); + } + MqttClientOptions = builder.Build(); + } + + private Task OnMessageReceived(MqttApplicationMessageReceivedEventArgs args) + { + try + { + if (IsDisposed) return Task.CompletedTask; + + var stringData = Encoding.UTF8.GetString(args.ApplicationMessage.Payload); + VDA5050Topic topic = EnumExtensions.ToTopic(args.ApplicationMessage.Topic); + + if (topic == VDA5050Topic.ORDER) OrderChanged?.Invoke(stringData); + else if (topic == VDA5050Topic.INSTANTACTIONS) InstanceActionsChanged?.Invoke(stringData); + } + catch (Exception ex) + { + Logger.Error($"Lỗi khi xử lý message: {ex.Message}"); + } + return Task.CompletedTask; + } + public async Task SubscribeAsync(CancellationToken cancellationToken = default) { - if (MqttClient is null) throw new Exception("Kết nối tới broker chưa được khởi tạo nhưng đã yêu cầu subscribe"); - if(!MqttClient.IsConnected) throw new Exception("Kết nối tới broker chưa thành công nhưng đã yêu cầu subscribe"); - - MqttClient.ApplicationMessageReceivedAsync += delegate (MqttApplicationMessageReceivedEventArgs args) + if (!IsDisposed) { - var stringData = Encoding.UTF8.GetString(args.ApplicationMessage.Payload); - VDA5050Topic topic = EnumExtensions.ToTopic(args.ApplicationMessage.Topic); - if (topic == VDA5050Topic.ORDER) OrderChanged?.Invoke(stringData); - else if (topic == VDA5050Topic.INSTANTACTIONS) InstanceActionsChanged?.Invoke(stringData); - return Task.CompletedTask; - }; + if (MqttClient is null) throw new Exception("Kết nối tới broker chưa được khởi tạo nhưng đã yêu cầu subscribe"); + if (!MqttClient.IsConnected) throw new Exception("Kết nối tới broker chưa thành công nhưng đã yêu cầu subscribe"); - while (!cancellationToken.IsCancellationRequested) - { - try + MqttClient.ApplicationMessageReceivedAsync -= OnMessageReceived; + MqttClient.ApplicationMessageReceivedAsync += OnMessageReceived; + + while (!cancellationToken.IsCancellationRequested) { - var response = await MqttClient.SubscribeAsync(MqttClientSubscribeOptions, cancellationToken); - bool isSuccess = true; - foreach (var item in response.Items) + try { - if (item.ResultCode == MqttClientSubscribeResultCode.GrantedQoS0 || - item.ResultCode == MqttClientSubscribeResultCode.GrantedQoS1 || - item.ResultCode == MqttClientSubscribeResultCode.GrantedQoS2) + var response = await MqttClient.SubscribeAsync(MqttClientSubscribeOptions, cancellationToken); + bool isSuccess = true; + foreach (var item in response.Items) { - Logger.Info($"Subscribe thành công cho topic: {item.TopicFilter.Topic} với QoS: {item.ResultCode}"); - } - else - { - Logger.Warning($"Subscribe thất bại cho topic: {item.TopicFilter.Topic}. Lý do: {response.ReasonString}"); - isSuccess = false; - break; + if (item.ResultCode == MqttClientSubscribeResultCode.GrantedQoS0 || + item.ResultCode == MqttClientSubscribeResultCode.GrantedQoS1 || + item.ResultCode == MqttClientSubscribeResultCode.GrantedQoS2) + { + Logger.Info($"Subscribe thành công cho topic: {item.TopicFilter.Topic} với QoS: {item.ResultCode}"); + } + else + { + Logger.Warning($"Subscribe thất bại cho topic: {item.TopicFilter.Topic}. Lý do: {response.ReasonString}"); + isSuccess = false; + break; + } } + if (isSuccess) break; + } + catch (Exception ex) + { + Logger.Error($"Lỗi khi subscribe: {ex.Message}"); + } + if (!cancellationToken.IsCancellationRequested && !IsDisposed) + { + await Task.Delay(3000, cancellationToken); } - if (isSuccess) break; } - catch (Exception ex) - { - Logger.Error($"Lỗi khi subscribe: {ex.Message}"); - } - await Task.Delay(3000, cancellationToken); } + else throw new ObjectDisposedException(nameof(MQTTClient)); } public async Task PublishAsync(string topic, string data) { + if (IsDisposed) return new(false, "Client đã được disposed"); var repeat = VDA5050Setting.PublishRepeat; - while (repeat-- > 0) + while (repeat-- > 0 && !IsDisposed) { try { @@ -149,12 +292,17 @@ public class MQTTClient : IAsyncDisposable public async ValueTask DisposeAsync() { - if (MqttClient is not null) + if (IsDisposed) return; + IsDisposed = true; + await ReconnectionSemaphore.WaitAsync(); + try { - if (MqttClient.IsConnected) await MqttClient.DisconnectAsync(); - MqttClient.Dispose(); - MqttClient = null; + await CleanupCurrentClient(); + } + finally + { + ReconnectionSemaphore.Dispose(); + GC.SuppressFinalize(this); } - GC.SuppressFinalize(this); } } diff --git a/RobotApp/Services/Robot/RobotConfiguration.cs b/RobotApp/Services/Robot/RobotConfiguration.cs index 6801e3b..adfd255 100644 --- a/RobotApp/Services/Robot/RobotConfiguration.cs +++ b/RobotApp/Services/Robot/RobotConfiguration.cs @@ -1,7 +1,6 @@ using RobotApp.Common.Shares.Enums; using RobotApp.Interfaces; using RobotApp.Services.Robot.Simulation; -using RobotApp.VDA5050.State; namespace RobotApp.Services.Robot; diff --git a/RobotApp/Services/State/RobotStateMachine.cs b/RobotApp/Services/State/RobotStateMachine.cs index d6085f3..f96bd8f 100644 --- a/RobotApp/Services/State/RobotStateMachine.cs +++ b/RobotApp/Services/State/RobotStateMachine.cs @@ -3,7 +3,7 @@ using System.Collections.Concurrent; namespace RobotApp.Services.State; -public record RobotStateMachine(Logger Logger) : IDisposable +public class RobotStateMachine(Logger Logger) : IDisposable { private readonly Lock StateLock = new(); private readonly ConcurrentDictionary> StateRegistry = []; diff --git a/RobotApp/robot.db b/RobotApp/robot.db index 56f2f75..1300b22 100644 Binary files a/RobotApp/robot.db and b/RobotApp/robot.db differ