using System; using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using SharpCAT.Client.Models; namespace SharpCAT.Client { /// /// Client for accessing SharpCAT Server API endpoints /// public class SharpCATClient : IDisposable { private readonly HttpClient _httpClient; private readonly bool _ownsHttpClient; /// /// Gets the base address of the SharpCAT server /// public Uri BaseAddress => _httpClient.BaseAddress ?? throw new InvalidOperationException("Base address not set"); /// /// Initializes a new instance of the SharpCATClient with the specified base address /// /// Base URL of the SharpCAT server (e.g., "http://localhost:5188") public SharpCATClient(string baseAddress) : this(new Uri(baseAddress)) { } /// /// Initializes a new instance of the SharpCATClient with the specified base address /// /// Base URI of the SharpCAT server public SharpCATClient(Uri baseAddress) { _httpClient = new HttpClient { BaseAddress = baseAddress }; _ownsHttpClient = true; } /// /// Initializes a new instance of the SharpCATClient with an existing HttpClient /// /// Configured HttpClient instance public SharpCATClient(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _ownsHttpClient = false; } /// /// Gets all available serial ports on the system /// /// Cancellation token /// List of available serial ports /// Thrown when the API call fails public async Task GetPortsAsync(CancellationToken cancellationToken = default) { try { var response = await _httpClient.GetFromJsonAsync("api/cat/ports", cancellationToken); return response ?? throw new SharpCATClientException("Failed to deserialize response"); } catch (HttpRequestException ex) { throw new SharpCATClientException("Failed to get ports", ex); } catch (TaskCanceledException ex) { throw new SharpCATClientException("Request timed out", ex); } } /// /// Opens and configures a serial port for communication /// /// Port configuration parameters /// Cancellation token /// Result of the port open operation /// Thrown when request is null /// Thrown when the API call fails public async Task OpenPortAsync(OpenPortRequest request, CancellationToken cancellationToken = default) { if (request == null) throw new ArgumentNullException(nameof(request)); try { var httpResponse = await _httpClient.PostAsJsonAsync("api/cat/open", request, cancellationToken); if (httpResponse.IsSuccessStatusCode) { var response = await httpResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); return response ?? throw new SharpCATClientException("Failed to deserialize response"); } else { var errorResponse = await httpResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); throw new SharpCATClientException($"API returned error: {errorResponse?.Error ?? "Unknown error"}", errorResponse?.Details); } } catch (HttpRequestException ex) { throw new SharpCATClientException("Failed to open port", ex); } catch (TaskCanceledException ex) { throw new SharpCATClientException("Request timed out", ex); } } /// /// Closes the currently opened serial port /// /// Cancellation token /// Result of the port close operation /// Thrown when the API call fails public async Task ClosePortAsync(CancellationToken cancellationToken = default) { try { var httpResponse = await _httpClient.PostAsync("api/cat/close", null, cancellationToken); if (httpResponse.IsSuccessStatusCode) { var response = await httpResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); return response ?? throw new SharpCATClientException("Failed to deserialize response"); } else { var errorResponse = await httpResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); throw new SharpCATClientException($"API returned error: {errorResponse?.Error ?? "Unknown error"}", errorResponse?.Details); } } catch (HttpRequestException ex) { throw new SharpCATClientException("Failed to close port", ex); } catch (TaskCanceledException ex) { throw new SharpCATClientException("Request timed out", ex); } } /// /// Gets the current status of the serial port connection /// /// Cancellation token /// Current port status /// Thrown when the API call fails public async Task GetStatusAsync(CancellationToken cancellationToken = default) { try { var response = await _httpClient.GetFromJsonAsync("api/cat/status", cancellationToken); return response ?? throw new SharpCATClientException("Failed to deserialize response"); } catch (HttpRequestException ex) { throw new SharpCATClientException("Failed to get status", ex); } catch (TaskCanceledException ex) { throw new SharpCATClientException("Request timed out", ex); } } /// /// Sends a CAT command to the connected radio /// /// CAT command to send /// Cancellation token /// Result of the command operation including any response from the radio /// Thrown when request is null /// Thrown when the API call fails public async Task SendCommandAsync(SendCommandRequest request, CancellationToken cancellationToken = default) { if (request == null) throw new ArgumentNullException(nameof(request)); try { var httpResponse = await _httpClient.PostAsJsonAsync("api/cat/command", request, cancellationToken); if (httpResponse.IsSuccessStatusCode) { var response = await httpResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); return response ?? throw new SharpCATClientException("Failed to deserialize response"); } else { var errorResponse = await httpResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); throw new SharpCATClientException($"API returned error: {errorResponse?.Error ?? "Unknown error"}", errorResponse?.Details); } } catch (HttpRequestException ex) { throw new SharpCATClientException("Failed to send command", ex); } catch (TaskCanceledException ex) { throw new SharpCATClientException("Request timed out", ex); } } /// /// Sends a CAT command to the connected radio /// /// CAT command string to send /// Cancellation token /// Result of the command operation including any response from the radio /// Thrown when command is null /// Thrown when the API call fails public async Task SendCommandAsync(string command, CancellationToken cancellationToken = default) { if (command == null) throw new ArgumentNullException(nameof(command)); var request = new SendCommandRequest { Command = command }; return await SendCommandAsync(request, cancellationToken); } /// /// Gets the current frequency from the radio (VFO A) /// /// Cancellation token /// Current frequency in Hz, or null if the command failed or returned no data /// Thrown when the API call fails public async Task GetFrequencyAsync(CancellationToken cancellationToken = default) { var response = await SendCommandAsync("FA;", cancellationToken); if (!response.Success || string.IsNullOrEmpty(response.Response)) return null; // Parse frequency response: FA00014074000; -> 14074000 Hz var frequencyStr = response.Response; if (!string.IsNullOrEmpty(frequencyStr) && frequencyStr.StartsWith("FA") && frequencyStr.EndsWith(";")) { var freqPart = frequencyStr.Substring(2, frequencyStr.Length - 3); if (long.TryParse(freqPart, out var frequency)) { return frequency; } } return null; } /// /// Sets the frequency for the radio (VFO A) /// /// Frequency in Hz /// Cancellation token /// True if the command was sent successfully /// Thrown when frequency is out of valid range /// Thrown when the API call fails public async Task SetFrequencyAsync(long frequencyHz, CancellationToken cancellationToken = default) { if (frequencyHz < 0 || frequencyHz > 999999999999) throw new ArgumentOutOfRangeException(nameof(frequencyHz), "Frequency must be between 0 and 999,999,999,999 Hz"); // Format frequency as 11-digit string with leading zeros var command = $"FA{frequencyHz:D11};"; var response = await SendCommandAsync(command, cancellationToken); return response.Success; } /// /// Gets the current frequency from the radio (VFO B) /// /// Cancellation token /// Current frequency in Hz, or null if the command failed or returned no data /// Thrown when the API call fails public async Task GetFrequencyBAsync(CancellationToken cancellationToken = default) { var response = await SendCommandAsync("FB;", cancellationToken); if (!response.Success || string.IsNullOrEmpty(response.Response)) return null; // Parse frequency response: FB00014074000; -> 14074000 Hz var frequencyStr = response.Response; if (!string.IsNullOrEmpty(frequencyStr) && frequencyStr.StartsWith("FB") && frequencyStr.EndsWith(";")) { var freqPart = frequencyStr.Substring(2, frequencyStr.Length - 3); if (long.TryParse(freqPart, out var frequency)) { return frequency; } } return null; } /// /// Sets the frequency for the radio (VFO B) /// /// Frequency in Hz /// Cancellation token /// True if the command was sent successfully /// Thrown when frequency is out of valid range /// Thrown when the API call fails public async Task SetFrequencyBAsync(long frequencyHz, CancellationToken cancellationToken = default) { if (frequencyHz < 0 || frequencyHz > 999999999999) throw new ArgumentOutOfRangeException(nameof(frequencyHz), "Frequency must be between 0 and 999,999,999,999 Hz"); // Format frequency as 11-digit string with leading zeros var command = $"FB{frequencyHz:D11};"; var response = await SendCommandAsync(command, cancellationToken); return response.Success; } /// /// Disposes the client and releases resources /// public void Dispose() { if (_ownsHttpClient) { _httpClient?.Dispose(); } } } }