Files
Denso/srcs/RobotNet10/Components/RobotNet10.NavigationTuneUI/Services/TuningApiService.cs
2026-07-03 16:31:37 +07:00

1144 lines
39 KiB
C#

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;
/// <summary>
/// API service for Navigation Tuning backend
/// </summary>
public class TuningApiService(HttpClient httpClient, ILogger<TuningApiService>? logger = null)
{
private readonly HttpClient _httpClient = httpClient;
private readonly ILogger<TuningApiService>? _logger = logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
#region Parameter Sets
/// <summary>
/// Get all parameter sets
/// </summary>
public async Task<List<NavigationParameterSet>> 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<List<NavigationParameterSet>>(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;
}
}
/// <summary>
/// Get parameter set by ID
/// </summary>
public async Task<NavigationParameterSet?> GetParameterSetAsync(Guid id)
{
try
{
return await _httpClient.GetFromJsonAsync<NavigationParameterSet>($"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;
}
}
/// <summary>
/// Get parameter set by name
/// </summary>
public async Task<NavigationParameterSet?> GetParameterSetByNameAsync(string name)
{
try
{
return await _httpClient.GetFromJsonAsync<NavigationParameterSet>($"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;
}
}
/// <summary>
/// Create new parameter set
/// </summary>
public async Task<NavigationParameterSet> 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<NavigationParameterSet>()
?? 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);
}
}
/// <summary>
/// Update parameter set
/// </summary>
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);
}
}
/// <summary>
/// Delete parameter set
/// </summary>
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);
}
}
/// <summary>
/// Validate parameter set
/// </summary>
public async Task<ValidationResult> 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<ValidationResult>()
?? 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);
}
}
/// <summary>
/// Get default preset
/// </summary>
public async Task<NavigationParameterSet> GetDefaultPresetAsync()
{
try
{
return await _httpClient.GetFromJsonAsync<NavigationParameterSet>("api/parametersets/presets/default")
?? throw new InvalidOperationException("Failed to get default preset");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error getting default preset");
throw;
}
}
/// <summary>
/// Get aggressive preset
/// </summary>
public async Task<NavigationParameterSet> GetAggressivePresetAsync()
{
try
{
return await _httpClient.GetFromJsonAsync<NavigationParameterSet>("api/parametersets/presets/aggressive")
?? throw new InvalidOperationException("Failed to get aggressive preset");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error getting aggressive preset");
throw;
}
}
/// <summary>
/// Get smooth preset
/// </summary>
public async Task<NavigationParameterSet> GetSmoothPresetAsync()
{
try
{
return await _httpClient.GetFromJsonAsync<NavigationParameterSet>("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
/// <summary>
/// DTO for TestScenario API responses
/// </summary>
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;
}
/// <summary>
/// Request DTO for Create/Update scenario (avoids sending abstract TestScenario to API).
/// </summary>
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;
}
/// <summary>
/// Get all scenarios
/// </summary>
public async Task<List<TestScenario>> GetScenariosAsync()
{
try
{
var dtos = await _httpClient.GetFromJsonAsync<List<TestScenarioDto>>("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);
}
}
/// <summary>
/// Get default scenarios
/// </summary>
public async Task<List<TestScenario>> GetDefaultScenariosAsync()
{
try
{
var dtos = await _httpClient.GetFromJsonAsync<List<TestScenarioDto>>("api/scenarios/defaults");
if (dtos == null)
return new List<TestScenario>();
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);
}
}
/// <summary>
/// Get scenario by ID
/// </summary>
public async Task<TestScenario?> GetScenarioAsync(Guid id)
{
try
{
var dto = await _httpClient.GetFromJsonAsync<TestScenarioDto>($"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);
}
}
/// <summary>
/// Convert DTO to TestScenario instance using TestScenarioEntity conversion logic
/// Note: This requires reference to NavigationTune project for concrete scenario types
/// </summary>
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<StraightLineScenario>(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<CircleScenario>(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<CustomPathScenario>(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")
};
}
/// <summary>
/// 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).
/// </summary>
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),
_ => "{}"
};
}
/// <summary>
/// Create new scenario. Sends Type + ConfigJson to avoid abstract type serialization.
/// </summary>
public async Task<TestScenario> 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<TestScenarioDto>();
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);
}
}
/// <summary>
/// Update scenario. Sends Type + ConfigJson to avoid abstract type serialization.
/// </summary>
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);
}
}
/// <summary>
/// Delete scenario
/// </summary>
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
/// <summary>
/// Get all test runs (backward compat: returns items only, uses limit if provided)
/// </summary>
public async Task<List<TestRunDto>> GetTestRunsAsync(int? limit = null)
{
var paged = await GetTestRunsPagedAsync(0, limit ?? 50);
return paged.Items;
}
/// <summary>
/// Get test runs with pagination
/// </summary>
public async Task<PagedResult<TestRunDto>> GetTestRunsPagedAsync(int skip, int take)
{
try
{
var url = $"api/testruns?skip={skip}&take={take}";
var response = await _httpClient.GetFromJsonAsync<PagedResult<TestRunDto>>(url);
return response ?? new PagedResult<TestRunDto>();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error getting test runs paged");
throw;
}
}
/// <summary>
/// Get test run by ID
/// </summary>
public async Task<TestRunDto?> GetTestRunAsync(Guid id)
{
try
{
return await _httpClient.GetFromJsonAsync<TestRunDto>($"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;
}
}
/// <summary>
/// Get test runs by scenario
/// </summary>
public async Task<List<TestRunDto>> GetTestRunsByScenarioAsync(Guid scenarioId)
{
try
{
var response = await _httpClient.GetFromJsonAsync<List<TestRunDto>>($"api/testruns/scenario/{scenarioId}");
return response ?? new List<TestRunDto>();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error getting test runs for scenario {ScenarioId}", scenarioId);
throw;
}
}
/// <summary>
/// Get test runs by parameter set
/// </summary>
public async Task<List<TestRunDto>> GetTestRunsByParameterSetAsync(Guid parameterSetId)
{
try
{
var response = await _httpClient.GetFromJsonAsync<List<TestRunDto>>($"api/testruns/parameterset/{parameterSetId}");
return response ?? new List<TestRunDto>();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error getting test runs for parameter set {ParameterSetId}", parameterSetId);
throw;
}
}
/// <summary>
/// Get test runs by date range
/// </summary>
public async Task<List<TestRunDto>> GetTestRunsByDateRangeAsync(DateTime from, DateTime to)
{
try
{
var response = await _httpClient.GetFromJsonAsync<List<TestRunDto>>(
$"api/testruns/daterange?from={from:yyyy-MM-ddTHH:mm:ss}&to={to:yyyy-MM-ddTHH:mm:ss}");
return response ?? new List<TestRunDto>();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error getting test runs for date range");
throw;
}
}
/// <summary>
/// Delete test run
/// </summary>
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;
}
}
/// <summary>
/// Delete multiple test runs by IDs
/// </summary>
public async Task DeleteTestRunsAsync(IEnumerable<Guid> ids)
{
var idList = ids?.ToList() ?? new List<Guid>();
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
/// <summary>
/// Execute a single test
/// </summary>
/// <summary>
/// Execute a single test. Pass testRunId and call JoinTestSession(testRunId) before this to receive real-time telemetry.
/// </summary>
public async Task<TestExecutionResult> 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<TestExecutionResult>()
?? 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);
}
}
/// <summary>
/// Run batch tests
/// </summary>
public async Task<BatchTestResult> RunBatchTestsAsync(List<Guid> 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<BatchTestResult>()
?? 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);
}
}
/// <summary>
/// Compare configurations
/// </summary>
public async Task<ComparisonResult> CompareConfigurationsAsync(Guid scenarioId, List<Guid> 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<ComparisonResult>()
?? 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);
}
}
/// <summary>
/// Pause test
/// </summary>
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);
}
}
/// <summary>
/// Resume test
/// </summary>
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);
}
}
/// <summary>
/// Stop test
/// </summary>
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);
}
}
/// <summary>
/// Emergency stop
/// </summary>
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
/// <summary>
/// Re-analyze a completed test run and generate tuning suggestions
/// </summary>
public async Task<TuningReport?> 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<TuningReport>(JsonOptions);
}
catch (ApiException)
{
throw;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error analyzing test run {TestRunId}", testRunId);
throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex);
}
}
/// <summary>
/// Apply selected suggestions to a parameter set (preview only, does not persist)
/// </summary>
public async Task<NavigationParameterSet?> ApplySuggestionsAsync(
Guid parameterSetId, List<Guid> 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<NavigationParameterSet>(JsonOptions);
}
catch (ApiException)
{
throw;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error applying suggestions");
throw new ApiException(ErrorHelper.GetErrorMessage(ex), ex);
}
}
/// <summary>
/// Apply selected suggestions and save as a new parameter set
/// </summary>
public async Task<NavigationParameterSet?> ApplyAndSaveSuggestionsAsync(
Guid parameterSetId, List<Guid> 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<NavigationParameterSet>(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
/// <summary>
/// Set manual velocity command
/// </summary>
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);
}
}
/// <summary>
/// Stop all velocities
/// </summary>
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);
}
}
/// <summary>
/// Get current velocity
/// </summary>
public async Task<(double Linear, double Angular)> GetCurrentVelocityAsync()
{
try
{
var response = await _httpClient.GetFromJsonAsync<VelocityResponse>("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
}