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
}