using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.Logging; using RobotNet10.NavigationTune.Shared.Interfaces; using RobotNet10.NavigationTune.Shared.Models; using RobotNet10.NavigationTuneUI.Helpers; namespace RobotNet10.NavigationTuneUI.Services; /// /// API service for Navigation Tuning backend /// public class TuningApiService(HttpClient httpClient, ILogger? logger = null) { private readonly HttpClient _httpClient = httpClient; private readonly ILogger? _logger = logger; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; #region Parameter Sets /// /// Get all parameter sets /// public async Task> GetParameterSetsAsync() { try { var response = await _httpClient.GetAsync("api/parametersets"); if (!response.IsSuccessStatusCode) { _logger?.LogWarning("Get parameter sets returned {StatusCode}", response.StatusCode); return []; } var content = await response.Content.ReadAsStringAsync(); if (string.IsNullOrWhiteSpace(content)) return []; try { var result = JsonSerializer.Deserialize>(content, JsonOptions); return result ?? []; } catch (JsonException jsonEx) { _logger?.LogWarning(jsonEx, "Parameter sets response was not valid JSON. Content length: {Length}", content.Length); return []; } } catch (Exception ex) { _logger?.LogError(ex, "Error getting parameter sets"); throw; } } /// /// Get parameter set by ID /// public async Task GetParameterSetAsync(Guid id) { try { return await _httpClient.GetFromJsonAsync($"api/parametersets/{id}"); } catch (HttpRequestException ex) when (ex.Message.Contains("404")) { return null; } catch (Exception ex) { _logger?.LogError(ex, "Error getting parameter set {Id}", id); throw; } } /// /// Get parameter set by name /// public async Task GetParameterSetByNameAsync(string name) { try { return await _httpClient.GetFromJsonAsync($"api/parametersets/name/{Uri.EscapeDataString(name)}"); } catch (HttpRequestException ex) when (ex.Message.Contains("404")) { return null; } catch (Exception ex) { _logger?.LogError(ex, "Error getting parameter set {Name}", name); throw; } } /// /// Create new parameter set /// public async Task CreateParameterSetAsync(NavigationParameterSet parameterSet) { try { var response = await _httpClient.PostAsJsonAsync("api/parametersets", parameterSet); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } return await response.Content.ReadFromJsonAsync() ?? throw new InvalidOperationException("Không thể đọc dữ liệu từ máy chủ"); } catch (ApiException) { throw; // Re-throw ApiException as-is } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error creating parameter set"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error creating parameter set"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Update parameter set /// public async Task UpdateParameterSetAsync(Guid id, NavigationParameterSet parameterSet) { try { var response = await _httpClient.PutAsJsonAsync($"api/parametersets/{id}", parameterSet); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error updating parameter set {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error updating parameter set {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Delete parameter set /// public async Task DeleteParameterSetAsync(Guid id) { try { var response = await _httpClient.DeleteAsync($"api/parametersets/{id}"); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error deleting parameter set {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error deleting parameter set {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Validate parameter set /// public async Task ValidateParameterSetAsync(NavigationParameterSet parameterSet) { try { var response = await _httpClient.PostAsJsonAsync("api/parametersets/validate", parameterSet); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } return await response.Content.ReadFromJsonAsync() ?? new ValidationResult { IsValid = false, Errors = ["Không thể đọc kết quả validation từ máy chủ"] }; } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error validating parameter set"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error validating parameter set"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Get default preset /// public async Task GetDefaultPresetAsync() { try { return await _httpClient.GetFromJsonAsync("api/parametersets/presets/default") ?? throw new InvalidOperationException("Failed to get default preset"); } catch (Exception ex) { _logger?.LogError(ex, "Error getting default preset"); throw; } } /// /// Get aggressive preset /// public async Task GetAggressivePresetAsync() { try { return await _httpClient.GetFromJsonAsync("api/parametersets/presets/aggressive") ?? throw new InvalidOperationException("Failed to get aggressive preset"); } catch (Exception ex) { _logger?.LogError(ex, "Error getting aggressive preset"); throw; } } /// /// Get smooth preset /// public async Task GetSmoothPresetAsync() { try { return await _httpClient.GetFromJsonAsync("api/parametersets/presets/smooth") ?? throw new InvalidOperationException("Failed to get smooth preset"); } catch (Exception ex) { _logger?.LogError(ex, "Error getting smooth preset"); throw; } } #endregion #region Scenarios /// /// DTO for TestScenario API responses /// private class TestScenarioDto { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public TrajectoryType Type { get; set; } public DateTime CreatedAt { get; set; } public bool IsDefault { get; set; } public string ConfigJson { get; set; } = string.Empty; } /// /// Request DTO for Create/Update scenario (avoids sending abstract TestScenario to API). /// private class CreateOrUpdateScenarioRequest { public Guid? Id { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public TrajectoryType Type { get; set; } public bool IsDefault { get; set; } public string ConfigJson { get; set; } = string.Empty; } /// /// Get all scenarios /// public async Task> GetScenariosAsync() { try { var dtos = await _httpClient.GetFromJsonAsync>("api/scenarios"); if (dtos == null) return []; return [.. dtos.Select(dto => ConvertDtoToScenario(dto))]; } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error getting scenarios"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error getting scenarios"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Get default scenarios /// public async Task> GetDefaultScenariosAsync() { try { var dtos = await _httpClient.GetFromJsonAsync>("api/scenarios/defaults"); if (dtos == null) return new List(); return dtos.Select(dto => ConvertDtoToScenario(dto)).ToList(); } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error getting default scenarios"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error getting default scenarios"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Get scenario by ID /// public async Task GetScenarioAsync(Guid id) { try { var dto = await _httpClient.GetFromJsonAsync($"api/scenarios/{id}"); return dto == null ? null : ConvertDtoToScenario(dto); } catch (HttpRequestException ex) when (ex.Message.Contains("404")) { return null; } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error getting scenario {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error getting scenario {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Convert DTO to TestScenario instance using TestScenarioEntity conversion logic /// Note: This requires reference to NavigationTune project for concrete scenario types /// private static TestScenario ConvertDtoToScenario(TestScenarioDto dto) { // Use the same conversion logic as TestScenarioEntity.ToTestScenario() return dto.Type switch { TrajectoryType.StraightLine => System.Text.Json.JsonSerializer.Deserialize(dto.ConfigJson) ?? new StraightLineScenario { Id = dto.Id, Name = dto.Name, Description = dto.Description, Type = dto.Type, CreatedAt = dto.CreatedAt, IsDefault = dto.IsDefault }, TrajectoryType.Circle => System.Text.Json.JsonSerializer.Deserialize(dto.ConfigJson) ?? new CircleScenario { Id = dto.Id, Name = dto.Name, Description = dto.Description, Type = dto.Type, CreatedAt = dto.CreatedAt, IsDefault = dto.IsDefault }, TrajectoryType.Custom => System.Text.Json.JsonSerializer.Deserialize(dto.ConfigJson) ?? new CustomPathScenario { Id = dto.Id, Name = dto.Name, Description = dto.Description, Type = dto.Type, CreatedAt = dto.CreatedAt, IsDefault = dto.IsDefault }, _ => throw new NotSupportedException($"Scenario type {dto.Type} is not supported") }; } /// /// Serialize scenario to ConfigJson by concrete type so all scenario-specific properties (StartX, Length, Edges, etc.) are included. /// Serializing as TestScenario can result in only base properties being sent (e.g. in Blazor/WASM). /// private static string SerializeScenarioToConfigJson(TestScenario scenario) { return scenario.Type switch { TrajectoryType.StraightLine => JsonSerializer.Serialize((StraightLineScenario)scenario), TrajectoryType.Circle => JsonSerializer.Serialize((CircleScenario)scenario), TrajectoryType.Custom => JsonSerializer.Serialize((CustomPathScenario)scenario), _ => "{}" }; } /// /// Create new scenario. Sends Type + ConfigJson to avoid abstract type serialization. /// public async Task CreateScenarioAsync(TestScenario scenario) { try { var request = new CreateOrUpdateScenarioRequest { Id = scenario.Id == Guid.Empty ? null : scenario.Id, Name = scenario.Name, Description = scenario.Description, Type = scenario.Type, IsDefault = scenario.IsDefault, ConfigJson = SerializeScenarioToConfigJson(scenario) }; var response = await _httpClient.PostAsJsonAsync("api/scenarios", request); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } var dto = await response.Content.ReadFromJsonAsync(); if (dto == null) throw new InvalidOperationException("Không thể đọc dữ liệu từ máy chủ"); return ConvertDtoToScenario(dto); } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error creating scenario"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error creating scenario"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Update scenario. Sends Type + ConfigJson to avoid abstract type serialization. /// public async Task UpdateScenarioAsync(Guid id, TestScenario scenario) { try { var request = new CreateOrUpdateScenarioRequest { Id = id, Name = scenario.Name, Description = scenario.Description, Type = scenario.Type, IsDefault = scenario.IsDefault, ConfigJson = SerializeScenarioToConfigJson(scenario) }; var response = await _httpClient.PutAsJsonAsync($"api/scenarios/{id}", request); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error updating scenario {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error updating scenario {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Delete scenario /// public async Task DeleteScenarioAsync(Guid id) { try { var response = await _httpClient.DeleteAsync($"api/scenarios/{id}"); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error deleting scenario {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error deleting scenario {Id}", id); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } #endregion #region Test Runs /// /// Get all test runs (backward compat: returns items only, uses limit if provided) /// public async Task> GetTestRunsAsync(int? limit = null) { var paged = await GetTestRunsPagedAsync(0, limit ?? 50); return paged.Items; } /// /// Get test runs with pagination /// public async Task> GetTestRunsPagedAsync(int skip, int take) { try { var url = $"api/testruns?skip={skip}&take={take}"; var response = await _httpClient.GetFromJsonAsync>(url); return response ?? new PagedResult(); } catch (Exception ex) { _logger?.LogError(ex, "Error getting test runs paged"); throw; } } /// /// Get test run by ID /// public async Task GetTestRunAsync(Guid id) { try { return await _httpClient.GetFromJsonAsync($"api/testruns/{id}"); } catch (HttpRequestException ex) when (ex.Message.Contains("404")) { return null; } catch (Exception ex) { _logger?.LogError(ex, "Error getting test run {Id}", id); throw; } } /// /// Get test runs by scenario /// public async Task> GetTestRunsByScenarioAsync(Guid scenarioId) { try { var response = await _httpClient.GetFromJsonAsync>($"api/testruns/scenario/{scenarioId}"); return response ?? new List(); } catch (Exception ex) { _logger?.LogError(ex, "Error getting test runs for scenario {ScenarioId}", scenarioId); throw; } } /// /// Get test runs by parameter set /// public async Task> GetTestRunsByParameterSetAsync(Guid parameterSetId) { try { var response = await _httpClient.GetFromJsonAsync>($"api/testruns/parameterset/{parameterSetId}"); return response ?? new List(); } catch (Exception ex) { _logger?.LogError(ex, "Error getting test runs for parameter set {ParameterSetId}", parameterSetId); throw; } } /// /// Get test runs by date range /// public async Task> GetTestRunsByDateRangeAsync(DateTime from, DateTime to) { try { var response = await _httpClient.GetFromJsonAsync>( $"api/testruns/daterange?from={from:yyyy-MM-ddTHH:mm:ss}&to={to:yyyy-MM-ddTHH:mm:ss}"); return response ?? new List(); } catch (Exception ex) { _logger?.LogError(ex, "Error getting test runs for date range"); throw; } } /// /// Delete test run /// public async Task DeleteTestRunAsync(Guid id) { try { var response = await _httpClient.DeleteAsync($"api/testruns/{id}"); response.EnsureSuccessStatusCode(); } catch (Exception ex) { _logger?.LogError(ex, "Error deleting test run {Id}", id); throw; } } /// /// Delete multiple test runs by IDs /// public async Task DeleteTestRunsAsync(IEnumerable ids) { var idList = ids?.ToList() ?? new List(); if (idList.Count == 0) return; try { var response = await _httpClient.PostAsJsonAsync("api/testruns/delete-batch", new DeleteBatchRequest { Ids = idList }); response.EnsureSuccessStatusCode(); } catch (Exception ex) { _logger?.LogError(ex, "Error deleting test runs batch"); throw; } } #endregion #region Test Execution /// /// Execute a single test /// /// /// Execute a single test. Pass testRunId and call JoinTestSession(testRunId) before this to receive real-time telemetry. /// public async Task ExecuteTestAsync(Guid scenarioId, Guid parameterSetId, Guid? testRunId = null, string? connectionId = null) { try { var request = new { ScenarioId = scenarioId, ParameterSetId = parameterSetId, TestRunId = testRunId }; var requestMessage = new HttpRequestMessage(HttpMethod.Post, "api/tuning/execute") { Content = JsonContent.Create(request) }; if (!string.IsNullOrEmpty(connectionId)) { requestMessage.Headers.Add("X-Connection-Id", connectionId); } var response = await _httpClient.SendAsync(requestMessage); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } return await response.Content.ReadFromJsonAsync() ?? throw new InvalidOperationException("Không thể đọc kết quả test từ máy chủ"); } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error executing test"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error executing test"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Run batch tests /// public async Task RunBatchTestsAsync(List scenarioIds, Guid parameterSetId) { try { var request = new { ScenarioIds = scenarioIds, ParameterSetId = parameterSetId }; var response = await _httpClient.PostAsJsonAsync("api/tuning/batch", request); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } return await response.Content.ReadFromJsonAsync() ?? throw new InvalidOperationException("Không thể đọc kết quả batch test từ máy chủ"); } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error running batch tests"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error running batch tests"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Compare configurations /// public async Task CompareConfigurationsAsync(Guid scenarioId, List parameterSetIds) { try { var request = new { ScenarioId = scenarioId, ParameterSetIds = parameterSetIds }; var response = await _httpClient.PostAsJsonAsync("api/tuning/compare", request); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } return await response.Content.ReadFromJsonAsync() ?? throw new InvalidOperationException("Không thể đọc kết quả so sánh từ máy chủ"); } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error comparing configurations"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error comparing configurations"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Pause test /// public async Task PauseTestAsync(string testRunId) { try { var response = await _httpClient.PostAsync($"api/tuning/pause/{testRunId}", null); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error pausing test {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error pausing test {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Resume test /// public async Task ResumeTestAsync(string testRunId) { try { var response = await _httpClient.PostAsync($"api/tuning/resume/{testRunId}", null); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error resuming test {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error resuming test {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Stop test /// public async Task StopTestAsync(string testRunId) { try { var response = await _httpClient.PostAsync($"api/tuning/stop/{testRunId}", null); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error stopping test {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error stopping test {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Emergency stop /// public async Task EmergencyStopAsync(string testRunId) { try { var response = await _httpClient.PostAsync($"api/tuning/emergency-stop/{testRunId}", null); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error emergency stopping test {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error emergency stopping test {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } #endregion #region Tuning Advisor /// /// Re-analyze a completed test run and generate tuning suggestions /// public async Task AnalyzeTestRunAsync(Guid testRunId) { try { var response = await _httpClient.PostAsync($"api/tuningadvisor/analyze/{testRunId}", null); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (ApiException) { throw; } catch (Exception ex) { _logger?.LogError(ex, "Error analyzing test run {TestRunId}", testRunId); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Apply selected suggestions to a parameter set (preview only, does not persist) /// public async Task ApplySuggestionsAsync( Guid parameterSetId, List suggestionIds, TuningReport report) { try { var request = new { ParameterSetId = parameterSetId, SuggestionIds = suggestionIds, Report = report }; var response = await _httpClient.PostAsJsonAsync("api/tuningadvisor/apply", request); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (ApiException) { throw; } catch (Exception ex) { _logger?.LogError(ex, "Error applying suggestions"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Apply selected suggestions and save as a new parameter set /// public async Task ApplyAndSaveSuggestionsAsync( Guid parameterSetId, List suggestionIds, TuningReport report, string? newName = null) { try { var request = new { ParameterSetId = parameterSetId, SuggestionIds = suggestionIds, Report = report, NewParameterSetName = newName }; var response = await _httpClient.PostAsJsonAsync("api/tuningadvisor/apply-and-save", request); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int)response.StatusCode); } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (ApiException) { throw; } catch (Exception ex) { _logger?.LogError(ex, "Error applying and saving suggestions"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } #endregion #region Velocity Control /// /// Set manual velocity command /// public async Task SetVelocityAsync(double linearVelocity, double angularVelocity) { try { var request = new { LinearVelocity = linearVelocity, AngularVelocity = angularVelocity }; var response = await _httpClient.PostAsJsonAsync("api/velocitycontrol/set-velocity", request); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int?)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error setting velocity"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error setting velocity"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Stop all velocities /// public async Task StopVelocityAsync() { try { var response = await _httpClient.PostAsync("api/velocitycontrol/stop", null); if (!response.IsSuccessStatusCode) { var errorMessage = await ErrorHelper.GetErrorMessageAsync(response); throw new ApiException(errorMessage, (int?)response.StatusCode); } } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error stopping velocity"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error stopping velocity"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } /// /// Get current velocity /// public async Task<(double Linear, double Angular)> GetCurrentVelocityAsync() { try { var response = await _httpClient.GetFromJsonAsync("api/velocitycontrol/current"); if (response == null) return (0, 0); return (response.Linear, response.Angular); } catch (ApiException) { throw; } catch (HttpRequestException ex) { _logger?.LogError(ex, "Error getting current velocity"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } catch (Exception ex) { _logger?.LogError(ex, "Error getting current velocity"); throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex); } } private class VelocityResponse { public double Linear { get; set; } public double Angular { get; set; } } #endregion }