From 1c02dc4cac223a6287a7cec7f2863eeddf8efba1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 Aug 2025 02:57:06 +0000 Subject: [PATCH 1/2] Initial plan From 99ac9e3b59b6bd696040474cc0f5d55238d89fc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 Aug 2025 03:06:27 +0000 Subject: [PATCH 2/2] Add SharpCAT.Client library with full API implementation Co-authored-by: ekinnee <1707617+ekinnee@users.noreply.github.com> --- Client/Models/ApiModels.cs | 216 ++++++++++++++++++++ Client/README.md | 196 ++++++++++++++++++ Client/SharpCAT.Client.csproj | 17 ++ Client/SharpCATClient.cs | 325 ++++++++++++++++++++++++++++++ Client/SharpCATClientException.cs | 60 ++++++ SharpCAT.sln | 6 + 6 files changed, 820 insertions(+) create mode 100644 Client/Models/ApiModels.cs create mode 100644 Client/README.md create mode 100644 Client/SharpCAT.Client.csproj create mode 100644 Client/SharpCATClient.cs create mode 100644 Client/SharpCATClientException.cs diff --git a/Client/Models/ApiModels.cs b/Client/Models/ApiModels.cs new file mode 100644 index 0000000..72c1c4f --- /dev/null +++ b/Client/Models/ApiModels.cs @@ -0,0 +1,216 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace SharpCAT.Client.Models +{ + /// + /// Parity settings for serial communication + /// + public enum Parity + { + /// + /// No parity checking + /// + None = 0, + /// + /// Odd parity checking + /// + Odd = 1, + /// + /// Even parity checking + /// + Even = 2, + /// + /// Mark parity checking + /// + Mark = 3, + /// + /// Space parity checking + /// + Space = 4 + } + + /// + /// Stop bits settings for serial communication + /// + public enum StopBits + { + /// + /// No stop bits + /// + None = 0, + /// + /// One stop bit + /// + One = 1, + /// + /// Two stop bits + /// + Two = 2, + /// + /// One and a half stop bits + /// + OnePointFive = 3 + } + + /// + /// Handshake settings for serial communication + /// + public enum Handshake + { + /// + /// No flow control + /// + None = 0, + /// + /// XOn/XOff software flow control + /// + XOnXOff = 1, + /// + /// RTS hardware flow control + /// + RequestToSend = 2, + /// + /// Both RTS and XOn/XOff flow control + /// + RequestToSendXOnXOff = 3 + } + + /// + /// Response model for listing available serial ports + /// + public class PortListResponse + { + /// + /// Array of available serial port names + /// + public string[] Ports { get; set; } = new string[0]; + + /// + /// Number of available ports + /// + public int Count => Ports.Length; + } + + /// + /// Request model for opening a serial port + /// + public class OpenPortRequest + { + /// + /// Name of the serial port to open (e.g., "COM1", "/dev/ttyUSB0") + /// + [Required] + public string PortName { get; set; } = string.Empty; + + /// + /// Baud rate for communication (default: 9600) + /// + public int BaudRate { get; set; } = 9600; + + /// + /// Parity setting (default: None) + /// + public Parity Parity { get; set; } = Parity.None; + + /// + /// Stop bits setting (default: One) + /// + public StopBits StopBits { get; set; } = StopBits.One; + + /// + /// Handshake setting (default: None) + /// + public Handshake Handshake { get; set; } = Handshake.None; + } + + /// + /// Response model for port operations + /// + public class PortOperationResponse + { + /// + /// Indicates if the operation was successful + /// + public bool Success { get; set; } + + /// + /// Human-readable message about the operation + /// + public string Message { get; set; } = string.Empty; + + /// + /// Name of the port that was operated on + /// + public string? PortName { get; set; } + + /// + /// Current status of the port (open/closed) + /// + public bool IsOpen { get; set; } + } + + /// + /// Request model for sending CAT commands + /// + public class SendCommandRequest + { + /// + /// CAT command string to send to the radio + /// + [Required] + public string Command { get; set; } = string.Empty; + } + + /// + /// Response model for CAT command operations + /// + public class CommandResponse + { + /// + /// Indicates if the command was sent successfully + /// + public bool Success { get; set; } + + /// + /// The command that was sent + /// + public string Command { get; set; } = string.Empty; + + /// + /// Response received from the radio (if any) + /// + public string? Response { get; set; } + + /// + /// Human-readable message about the operation + /// + public string Message { get; set; } = string.Empty; + + /// + /// Timestamp when the command was executed + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + } + + /// + /// Response model for errors + /// + public class ErrorResponse + { + /// + /// Error message + /// + public string Error { get; set; } = string.Empty; + + /// + /// Additional details about the error + /// + public string? Details { get; set; } + + /// + /// Timestamp when the error occurred + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/Client/README.md b/Client/README.md new file mode 100644 index 0000000..4dd3a4e --- /dev/null +++ b/Client/README.md @@ -0,0 +1,196 @@ +# SharpCAT.Client + +A .NET Standard 2.0 client library that provides a typed C# API for consuming the SharpCAT Server HTTP API endpoints. This library makes it easy to integrate CAT (Computer Aided Transceiver) control into your .NET applications. + +## Features + +- **Typed API**: Strongly-typed C# methods for all SharpCAT Server endpoints +- **Async/Await Support**: Modern async programming with cancellation token support +- **Error Handling**: Comprehensive error handling with custom exceptions +- **Cross-Platform**: .NET Standard 2.0 compatible (works with .NET Core, .NET Framework, .NET 5+) +- **HTTP Client Integration**: Uses System.Net.Http.Json for JSON serialization +- **Frequency Helpers**: Convenient methods for getting/setting radio frequencies +- **XML Documentation**: Full IntelliSense support with comprehensive documentation + +## Installation + +Add the SharpCAT.Client project reference to your application: + +```xml + +``` + +## Quick Start + +### Basic Usage + +```csharp +using SharpCAT.Client; +using SharpCAT.Client.Models; + +// Create client instance +using var client = new SharpCATClient("http://localhost:5188"); + +// Get available serial ports +var ports = await client.GetPortsAsync(); +Console.WriteLine($"Available ports: {string.Join(", ", ports.Ports)}"); + +// Open a serial port +var openRequest = new OpenPortRequest +{ + PortName = "COM3", + BaudRate = 9600 +}; +var openResult = await client.OpenPortAsync(openRequest); +Console.WriteLine($"Port opened: {openResult.Success}"); + +// Send a raw CAT command +var commandResult = await client.SendCommandAsync("FA;"); +Console.WriteLine($"Command response: {commandResult.Response}"); + +// Use convenience methods for frequency operations +var frequency = await client.GetFrequencyAsync(); +Console.WriteLine($"Current frequency: {frequency} Hz"); + +await client.SetFrequencyAsync(14074000); // Set to 14.074 MHz +``` + +### Advanced Usage + +```csharp +// Use with dependency injection and HttpClientFactory +services.AddHttpClient(client => +{ + client.BaseAddress = new Uri("http://localhost:5188"); +}); + +// Handle errors +try +{ + var result = await client.SendCommandAsync("invalid_command"); +} +catch (SharpCATClientException ex) +{ + Console.WriteLine($"Error: {ex.Message}"); + if (ex.Details != null) + Console.WriteLine($"Details: {ex.Details}"); +} + +// Use cancellation tokens +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); +var status = await client.GetStatusAsync(cts.Token); +``` + +## API Reference + +### Core Methods + +#### Port Management +- `GetPortsAsync()` - Get all available serial ports +- `OpenPortAsync(request)` - Open and configure a serial port +- `ClosePortAsync()` - Close the current serial port +- `GetStatusAsync()` - Get current port connection status + +#### CAT Commands +- `SendCommandAsync(command)` - Send a raw CAT command string +- `SendCommandAsync(request)` - Send a CAT command with full request object + +#### Frequency Operations (Convenience Methods) +- `GetFrequencyAsync()` - Get current frequency (VFO A) +- `SetFrequencyAsync(frequencyHz)` - Set frequency (VFO A) +- `GetFrequencyBAsync()` - Get current frequency (VFO B) +- `SetFrequencyBAsync(frequencyHz)` - Set frequency (VFO B) + +### Configuration Options + +When opening a port, you can configure: + +```csharp +var request = new OpenPortRequest +{ + PortName = "COM3", // Required + BaudRate = 9600, // Default: 9600 + Parity = Parity.None, // Default: None + StopBits = StopBits.One, // Default: One + Handshake = Handshake.None // Default: None +}; +``` + +### Error Handling + +The client throws `SharpCATClientException` for various error conditions: + +- HTTP request failures +- Network timeouts +- API error responses +- Deserialization failures + +```csharp +try +{ + var result = await client.GetPortsAsync(); +} +catch (SharpCATClientException ex) +{ + // Handle SharpCAT-specific errors + Console.WriteLine($"SharpCAT Error: {ex.Message}"); +} +catch (Exception ex) +{ + // Handle other errors + Console.WriteLine($"Unexpected error: {ex.Message}"); +} +``` + +## Common CAT Commands + +Here are some common CAT commands you can send using `SendCommandAsync()`: + +| Command | Description | Example Response | +|---------|-------------|------------------| +| `FA;` | Get VFO A frequency | `FA00014074000;` | +| `FB;` | Get VFO B frequency | `FB00007074000;` | +| `FA14074000;` | Set VFO A frequency | No response or `FA14074000;` | +| `MD;` | Get operating mode | `MD2;` (USB) | +| `MD2;` | Set mode to USB | No response | +| `IF;` | Get radio information | `IF00014074000...;` | + +**Note**: The exact commands and responses depend on your radio model. Consult your radio's CAT documentation for complete command reference. + +## Thread Safety + +The `SharpCATClient` class is thread-safe for concurrent read operations, but write operations (like sending commands) should be serialized to avoid conflicts at the radio level. + +## Disposal + +The client implements `IDisposable`. When you create a client with a base address string or URI, it owns the internal `HttpClient` and will dispose it. If you pass in your own `HttpClient`, the client will not dispose it. + +```csharp +// Client owns HttpClient - will be disposed +using var client = new SharpCATClient("http://localhost:5188"); + +// You own HttpClient - manage disposal yourself +var httpClient = new HttpClient(); +var client = new SharpCATClient(httpClient); +// Don't forget to dispose httpClient when done +``` + +## Requirements + +- .NET Standard 2.0 or later +- SharpCAT Server running and accessible +- Network connectivity to the server + +## Dependencies + +- System.Net.Http.Json (5.0.0) +- System.Text.Json (5.0.2) +- System.ComponentModel.Annotations (5.0.0) + +## License + +This project follows the same license as the parent SharpCAT project. + +## Contributing + +Contributions are welcome! Please follow the existing code style and add appropriate documentation for new features. \ No newline at end of file diff --git a/Client/SharpCAT.Client.csproj b/Client/SharpCAT.Client.csproj new file mode 100644 index 0000000..6cf16d9 --- /dev/null +++ b/Client/SharpCAT.Client.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + enable + true + $(NoWarn);1591 + 9.0 + + + + + + + + + diff --git a/Client/SharpCATClient.cs b/Client/SharpCATClient.cs new file mode 100644 index 0000000..07b2670 --- /dev/null +++ b/Client/SharpCATClient.cs @@ -0,0 +1,325 @@ +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(); + } + } + } +} \ No newline at end of file diff --git a/Client/SharpCATClientException.cs b/Client/SharpCATClientException.cs new file mode 100644 index 0000000..070d3e6 --- /dev/null +++ b/Client/SharpCATClientException.cs @@ -0,0 +1,60 @@ +using System; + +namespace SharpCAT.Client +{ + /// + /// Exception thrown by SharpCAT client operations + /// + public class SharpCATClientException : Exception + { + /// + /// Additional details about the error + /// + public string? Details { get; } + + /// + /// Initializes a new instance of the SharpCATClientException class + /// + public SharpCATClientException() + { + } + + /// + /// Initializes a new instance of the SharpCATClientException class with a specified error message + /// + /// The message that describes the error + public SharpCATClientException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the SharpCATClientException class with a specified error message and details + /// + /// The message that describes the error + /// Additional details about the error + public SharpCATClientException(string message, string? details) : base(message) + { + Details = details; + } + + /// + /// Initializes a new instance of the SharpCATClientException class with a specified error message and a reference to the inner exception + /// + /// The message that describes the error + /// The exception that is the cause of the current exception + public SharpCATClientException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the SharpCATClientException class with a specified error message, details, and a reference to the inner exception + /// + /// The message that describes the error + /// Additional details about the error + /// The exception that is the cause of the current exception + public SharpCATClientException(string message, string? details, Exception innerException) : base(message, innerException) + { + Details = details; + } + } +} \ No newline at end of file diff --git a/SharpCAT.sln b/SharpCAT.sln index 02ee0d1..5be7bc7 100644 --- a/SharpCAT.sln +++ b/SharpCAT.sln @@ -9,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{43CFF6 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{22E4F655-252E-42DB-ADD5-494BC825A97C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCAT.Client", "Client\SharpCAT.Client.csproj", "{661C070A-612A-41CD-B9DC-9EF9FA05121A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {22E4F655-252E-42DB-ADD5-494BC825A97C}.Debug|Any CPU.Build.0 = Debug|Any CPU {22E4F655-252E-42DB-ADD5-494BC825A97C}.Release|Any CPU.ActiveCfg = Release|Any CPU {22E4F655-252E-42DB-ADD5-494BC825A97C}.Release|Any CPU.Build.0 = Release|Any CPU + {661C070A-612A-41CD-B9DC-9EF9FA05121A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {661C070A-612A-41CD-B9DC-9EF9FA05121A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {661C070A-612A-41CD-B9DC-9EF9FA05121A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {661C070A-612A-41CD-B9DC-9EF9FA05121A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {22E4F655-252E-42DB-ADD5-494BC825A97C} = {43CFF66C-84E6-4EC2-AE4F-005FB80D74D5}