From 73038de662fccf541393d47515c59f0bc9e4e0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=C4=83ng=20Nguy=E1=BB=85n?= Date: Tue, 4 Nov 2025 10:57:41 +0700 Subject: [PATCH] update --- .../Config/RobotSimulationConfig.razor | 100 +++++++++++------- .../Pages/RobotConfigManager.razor | 4 + .../Pages/RobotConfigManager.razor.cs | 29 +++-- .../Dtos/RobotSimulationConfigDto.cs | 9 +- .../Dtos/RobotVDA5050ConfigDto.cs | 2 + RobotApp.VDA5050/VDA5050Setting.cs | 7 +- RobotApp/Controllers/FileController.cs | 10 +- .../Controllers/RobotConfigsController.cs | 36 +++---- RobotApp/Services/MQTTClient.cs | 57 +++++++--- .../MQTTClientCertificatesProvider.cs | 38 +++++++ RobotApp/Services/Robot/RobotConfiguration.cs | 16 +++ .../Robot/RobotControllerInitialize.cs | 2 +- 12 files changed, 226 insertions(+), 84 deletions(-) create mode 100644 RobotApp/Services/MQTTClientCertificatesProvider.cs diff --git a/RobotApp.Client/Pages/Components/Config/RobotSimulationConfig.razor b/RobotApp.Client/Pages/Components/Config/RobotSimulationConfig.razor index a4f839d..b455f3d 100644 --- a/RobotApp.Client/Pages/Components/Config/RobotSimulationConfig.razor +++ b/RobotApp.Client/Pages/Components/Config/RobotSimulationConfig.razor @@ -1,40 +1,60 @@ @using RobotApp.Common.Shares.Dtos - - - -
- - -
+@implements IDisposable -
-
- - -
-
- - -
-
+
+ + -
-
- - +
+ + +
-
- - -
-
-
- - +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+ + + +
+ +
+
+ @if (Model.CreatedAt != default || Model.UpdatedAt != default) + { +
+ Created: @Model.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss") + Updated: @Model.UpdatedAt.ToString("dd/MM/yyyy HH:mm:ss") +
+ }
- +
@code { [Parameter] @@ -43,17 +63,25 @@ [Parameter] public EventCallback ModelChanged { get; set; } - private RobotSimulationConfigDto Local = new(); + private EditContext? EditContext; protected override void OnParametersSet() { - // Use record 'with' to create a shallow copy so parent isn't mutated until submit - Local = Model is not null ? Model with { } : new RobotSimulationConfigDto(); + 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 async Task OnSubmit() + private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e) { - Model = Local; - await ModelChanged.InvokeAsync(Model); + _ = ModelChanged.InvokeAsync(Model); + } + + public void Dispose() + { + if (EditContext is not null) EditContext.OnFieldChanged -= EditContext_OnFieldChanged; } } \ No newline at end of file diff --git a/RobotApp.Client/Pages/RobotConfigManager.razor b/RobotApp.Client/Pages/RobotConfigManager.razor index 016134f..a214fd2 100644 --- a/RobotApp.Client/Pages/RobotConfigManager.razor +++ b/RobotApp.Client/Pages/RobotConfigManager.razor @@ -39,6 +39,10 @@ + + diff --git a/RobotApp.Client/Pages/RobotConfigManager.razor.cs b/RobotApp.Client/Pages/RobotConfigManager.razor.cs index e34e059..83d9b72 100644 --- a/RobotApp.Client/Pages/RobotConfigManager.razor.cs +++ b/RobotApp.Client/Pages/RobotConfigManager.razor.cs @@ -306,7 +306,7 @@ public partial class RobotConfigManager } } - private async Task SaveCertificates(Guid id) + private async Task SaveCertificates() { using var content = new MultipartFormDataContent(); @@ -325,12 +325,16 @@ public partial class RobotConfigManager var fileContent = new StreamContent(RobotVDA5050ConfigRef.KeyFile.OpenReadStream(maxAllowedSize: RobotVDA5050ConfigRef.MaxFileSize)); content.Add(fileContent, "KeyFile", RobotVDA5050ConfigRef.KeyFile.Name); } - - var response = await (await Http.PostAsync($"api/File/certificates/{id}", content)).Content.ReadFromJsonAsync(); - if (response is null) Snackbar.Add("Failed to update certificates", Severity.Warning); - else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Failed to update certificates config", Severity.Warning); - else return true; - return false; + if (content.Any()) + { + var response = await (await Http.PostAsync($"api/File/certificates", content)).Content.ReadFromJsonAsync(); + 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() @@ -377,7 +381,7 @@ public partial class RobotConfigManager SelectedVda.VDA5050Key, SelectedVda.Description }; - var saveCer = await SaveCertificates(SelectedVda.Id); + var saveCer = await SaveCertificates(); if (saveCer) result = await (await Http.PutAsJsonAsync($"api/RobotConfigs/vda5050/{id}", updateDto)).Content.ReadFromJsonAsync(); else return; break; @@ -551,4 +555,13 @@ public partial class RobotConfigManager StateHasChanged(); } } + + private async Task LoadConfig() + { + var response = await (await Http.PostAsync($"api/RobotConfigs/load", null)).Content.ReadFromJsonAsync(); + 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); + StateHasChanged(); + } } diff --git a/RobotApp.Common.Shares/Dtos/RobotSimulationConfigDto.cs b/RobotApp.Common.Shares/Dtos/RobotSimulationConfigDto.cs index 5746876..114a309 100644 --- a/RobotApp.Common.Shares/Dtos/RobotSimulationConfigDto.cs +++ b/RobotApp.Common.Shares/Dtos/RobotSimulationConfigDto.cs @@ -1,4 +1,6 @@ -namespace RobotApp.Common.Shares.Dtos; +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.Common.Shares.Dtos; #nullable disable @@ -6,13 +8,18 @@ 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; } } diff --git a/RobotApp.Common.Shares/Dtos/RobotVDA5050ConfigDto.cs b/RobotApp.Common.Shares/Dtos/RobotVDA5050ConfigDto.cs index ac1ab76..dcb7d59 100644 --- a/RobotApp.Common.Shares/Dtos/RobotVDA5050ConfigDto.cs +++ b/RobotApp.Common.Shares/Dtos/RobotVDA5050ConfigDto.cs @@ -12,12 +12,14 @@ public record RobotVDA5050ConfigDto [Required] public string VDA5050HostServer { get; set; } [Required] + [Range(1, 65535, ErrorMessage = "Value must be from 1 to 65535")] public int VDA5050Port { get; set; } [Required] public string VDA5050UserName { get; set; } public string VDA5050Password { get; set; } public string VDA5050Manufacturer { get; set; } public string VDA5050Version { 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; } diff --git a/RobotApp.VDA5050/VDA5050Setting.cs b/RobotApp.VDA5050/VDA5050Setting.cs index 9473c9b..f450c7c 100644 --- a/RobotApp.VDA5050/VDA5050Setting.cs +++ b/RobotApp.VDA5050/VDA5050Setting.cs @@ -1,4 +1,6 @@ -namespace RobotApp.VDA5050; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RobotApp.VDA5050; public class VDA5050Setting @@ -12,4 +14,7 @@ public class VDA5050Setting 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; } } diff --git a/RobotApp/Controllers/FileController.cs b/RobotApp/Controllers/FileController.cs index 2b3d8e0..97ac685 100644 --- a/RobotApp/Controllers/FileController.cs +++ b/RobotApp/Controllers/FileController.cs @@ -15,8 +15,8 @@ public class FileController(Services.Logger Logger) : Controller private readonly string keyFilePath = "key"; [HttpPost] - [Route("certificates/{id:guid}")] - public async Task UpdateMqttCertificates(Guid id, [FromForm(Name = "CaFile")] IFormFile? caFile, + [Route("certificates")] + public async Task UpdateMqttCertificates([FromForm(Name = "CaFile")] IFormFile? caFile, [FromForm(Name = "CertFile")] IFormFile? certFile, [FromForm(Name = "KeyFile")] IFormFile? keyFile) { @@ -29,7 +29,7 @@ public class FileController(Services.Logger Logger) : Controller var caFolder = Path.Combine(certificatesPath, caFilePath); if (!Directory.Exists(caFolder)) Directory.CreateDirectory(caFolder); string caExtension = Path.GetExtension(caFile.FileName); - var caLocal = Path.Combine(caFolder, $"{id}{caExtension}"); + 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"); @@ -43,7 +43,7 @@ public class FileController(Services.Logger Logger) : Controller var certFolder = Path.Combine(certificatesPath, cerFilePath); if (!Directory.Exists(certFolder)) Directory.CreateDirectory(certFolder); string certExtension = Path.GetExtension(certFile.FileName); - var certLocal = Path.Combine(certFolder, $"{id}{certExtension}"); + 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"); @@ -57,7 +57,7 @@ public class FileController(Services.Logger Logger) : Controller var keyFolder = Path.Combine(certificatesPath, keyFilePath); if (!Directory.Exists(keyFolder)) Directory.CreateDirectory(keyFolder); string keyExtension = Path.GetExtension(keyFile.FileName); - var keyLocal = Path.Combine(keyFolder, $"{id}{keyExtension}"); + 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"); diff --git a/RobotApp/Controllers/RobotConfigsController.cs b/RobotApp/Controllers/RobotConfigsController.cs index 84fa62a..fd95fd2 100644 --- a/RobotApp/Controllers/RobotConfigsController.cs +++ b/RobotApp/Controllers/RobotConfigsController.cs @@ -63,8 +63,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - if (config.IsActive) await RobotConfiguration.LoadRobotPlcConfigAsync(); return new(true, "PLC configuration updated successfully."); } catch (Exception ex) @@ -158,8 +156,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - await RobotConfiguration.LoadRobotPlcConfigAsync(); return new(true, $"PLC configuration {config.ConfigName} activated successfully."); } catch (Exception ex) @@ -238,8 +234,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - if (config.IsActive) await RobotConfiguration.LoadVDA5050ConfigAsync(); return new(true, "VDA5050 configuration updated successfully."); } catch (Exception ex) @@ -353,8 +347,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - await RobotConfiguration.LoadVDA5050ConfigAsync(); return new(true, $"VDA5050 configuration {config.ConfigName} activated successfully."); } catch (Exception ex) @@ -417,8 +409,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - if (config.IsActive) await RobotConfiguration.LoadRobotConfigAsync(); return new(true, "Robot configuration updated successfully."); } catch (Exception ex) @@ -516,8 +506,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - await RobotConfiguration.LoadRobotConfigAsync(); return new(true, $"Robot configuration {config.ConfigName} activated successfully."); } catch (Exception ex) @@ -580,8 +568,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - if (config.IsActive) await RobotConfiguration.LoadRobotSimulationConfigAsync(); return new(true, "Simulation configuration updated successfully."); } catch (Exception ex) @@ -679,8 +665,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - await RobotConfiguration.LoadRobotSimulationConfigAsync(); return new(true, $"Simulation configuration {config.ConfigName} activated successfully."); } catch (Exception ex) @@ -746,8 +730,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - if (config.IsActive) await RobotConfiguration.LoadRobotSafetyConfigAsync(); return new(true, "Safety configuration updated successfully."); } catch (Exception ex) @@ -849,8 +831,6 @@ public class RobotConfigsController(Services.Logger Logg config.UpdatedAt = DateTime.Now; await AppDb.SaveChangesAsync(); - - await RobotConfiguration.LoadRobotSafetyConfigAsync(); return new(true, $"Safety configuration {config.ConfigName} activated successfully."); } catch (Exception ex) @@ -859,4 +839,20 @@ public class RobotConfigsController(Services.Logger Logg return new(false, "An error occurred while updating the active Safety configuration."); } } + + [HttpPost] + [Route("load")] + public async Task LoadConfig() + { + try + { + //await RobotConfiguration.LoadVDA5050ConfigAsync(); + 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."); + } + } } diff --git a/RobotApp/Services/MQTTClient.cs b/RobotApp/Services/MQTTClient.cs index d38af3e..b5be783 100644 --- a/RobotApp/Services/MQTTClient.cs +++ b/RobotApp/Services/MQTTClient.cs @@ -2,7 +2,7 @@ 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; @@ -45,7 +45,7 @@ public class MQTTClient : IAsyncDisposable private async Task OnDisconnected(MqttClientDisconnectedEventArgs args) { - if (IsDisposed || !args.ClientWasConnected) return; + if (IsDisposed || !args.ClientWasConnected) return; if (!await ReconnectionSemaphore.WaitAsync(0)) { @@ -128,7 +128,7 @@ public class MQTTClient : IAsyncDisposable if (attempt < maxRetries && !IsDisposed) { - await Task.Delay(retryDelayMs * attempt); + await Task.Delay(retryDelayMs * attempt); } } @@ -167,6 +167,46 @@ public class MQTTClient : IAsyncDisposable else throw new ObjectDisposedException(nameof(MQTTClient)); } + private bool ValidateCertificates(MqttClientCertificateValidationEventArgs arg) + { + string certificatesPath = "MqttCertificates"; + string caFilePath = "ca"; + + if (!string.IsNullOrEmpty(VDA5050Setting.CAFile)) + { + if (Directory.Exists(certificatesPath)) + { + var caFolder = Path.Combine(certificatesPath, caFilePath); + if (Directory.Exists(caFolder)) + { + var caLocal = Path.Combine(caFolder, VDA5050Setting.CAFile); + if (File.Exists(caLocal)) + { + var caCert = X509CertificateLoader.LoadCertificateFromFile(caLocal); + arg.Chain.ChainPolicy.ExtraStore.Add(caCert); + arg.Chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + arg.Chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + + var isValid = arg.Chain.Build((X509Certificate2)arg.Certificate); + + if (isValid) + { + Console.WriteLine("[BROKER] CLIENT CERTIFICATE VALID"); + } + else + { + Console.WriteLine("[BROKER] CLIENT CERTIFICATE INVALID"); + foreach (var status in arg.Chain.ChainStatus) + Console.WriteLine($" -> Chain error: {status.Status} - {status.StatusInformation}"); + } + return isValid; + } + } + } + } + return true; + } + private void BuildMqttClientOptions(bool enablePassword, bool enableTls) { var builder = MqttClientFactory.CreateClientOptionsBuilder() @@ -183,15 +223,8 @@ public class MQTTClient : IAsyncDisposable 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; - }) + .WithCertificateValidationHandler(ValidateCertificates) + .WithClientCertificatesProvider(new MQTTClientCertificatesProvider(VDA5050Setting.CerFile, VDA5050Setting.KeyFile)) .Build(); builder.WithTlsOptions(tlsOptions); } diff --git a/RobotApp/Services/MQTTClientCertificatesProvider.cs b/RobotApp/Services/MQTTClientCertificatesProvider.cs new file mode 100644 index 0000000..ccb2e9e --- /dev/null +++ b/RobotApp/Services/MQTTClientCertificatesProvider.cs @@ -0,0 +1,38 @@ +using MQTTnet; +using MQTTnet.Certificates; +using System.Security.Cryptography.X509Certificates; + +namespace RobotApp.Services; + +public class MQTTClientCertificatesProvider(string? CerFile, string? KeyFile) : IMqttClientCertificatesProvider +{ + private readonly string certificatesPath = "MqttCertificates"; + private readonly string cerFilePath = "cer"; + private readonly string keyFilePath = "key"; + + public X509CertificateCollection? GetCertificates() + { + if (!string.IsNullOrEmpty(CerFile) && !string.IsNullOrEmpty(KeyFile)) + { + if (Directory.Exists(certificatesPath)) + { + var certFolder = Path.Combine(certificatesPath, cerFilePath); + var keyFolder = Path.Combine(certificatesPath, keyFilePath); + if (Directory.Exists(certFolder) && Directory.Exists(keyFolder)) + { + var certLocal = Path.Combine(certFolder, CerFile); + var keyLocal = Path.Combine(keyFolder, KeyFile); + if (File.Exists(certLocal) && File.Exists(keyLocal)) + { + var cert = X509Certificate2.CreateFromPem(File.ReadAllText(certLocal), File.ReadAllText(keyLocal)); + var pfxBytes = cert.Export(X509ContentType.Pfx); + var pfxCert = X509CertificateLoader.LoadPkcs12(pfxBytes, "", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); + Console.WriteLine($"Client cert loaded: {pfxCert.Subject}, HasPrivateKey: {pfxCert.HasPrivateKey}, PrivateKey Type: {pfxCert.GetRSAPrivateKey()?.GetType()}"); + return [pfxCert]; + } + } + } + } + return null; + } +} diff --git a/RobotApp/Services/Robot/RobotConfiguration.cs b/RobotApp/Services/Robot/RobotConfiguration.cs index 2342977..8aeb486 100644 --- a/RobotApp/Services/Robot/RobotConfiguration.cs +++ b/RobotApp/Services/Robot/RobotConfiguration.cs @@ -49,6 +49,9 @@ public class RobotConfiguration(IServiceProvider ServiceProvider, Logger