1144 lines
39 KiB
C#
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
|
|
}
|