Merge pull request #23 from ekinnee/copilot/fix-edfc2c6e-7e60-445f-a0c3-de881f3f3755

Add SharpCAT.Client library for typed C# API access to Server endpoints
This commit is contained in:
Erick Kinnee 2025-08-06 22:10:52 -05:00 committed by GitHub
commit 416fcd98c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 820 additions and 0 deletions

216
Client/Models/ApiModels.cs Normal file
View file

@ -0,0 +1,216 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace SharpCAT.Client.Models
{
/// <summary>
/// Parity settings for serial communication
/// </summary>
public enum Parity
{
/// <summary>
/// No parity checking
/// </summary>
None = 0,
/// <summary>
/// Odd parity checking
/// </summary>
Odd = 1,
/// <summary>
/// Even parity checking
/// </summary>
Even = 2,
/// <summary>
/// Mark parity checking
/// </summary>
Mark = 3,
/// <summary>
/// Space parity checking
/// </summary>
Space = 4
}
/// <summary>
/// Stop bits settings for serial communication
/// </summary>
public enum StopBits
{
/// <summary>
/// No stop bits
/// </summary>
None = 0,
/// <summary>
/// One stop bit
/// </summary>
One = 1,
/// <summary>
/// Two stop bits
/// </summary>
Two = 2,
/// <summary>
/// One and a half stop bits
/// </summary>
OnePointFive = 3
}
/// <summary>
/// Handshake settings for serial communication
/// </summary>
public enum Handshake
{
/// <summary>
/// No flow control
/// </summary>
None = 0,
/// <summary>
/// XOn/XOff software flow control
/// </summary>
XOnXOff = 1,
/// <summary>
/// RTS hardware flow control
/// </summary>
RequestToSend = 2,
/// <summary>
/// Both RTS and XOn/XOff flow control
/// </summary>
RequestToSendXOnXOff = 3
}
/// <summary>
/// Response model for listing available serial ports
/// </summary>
public class PortListResponse
{
/// <summary>
/// Array of available serial port names
/// </summary>
public string[] Ports { get; set; } = new string[0];
/// <summary>
/// Number of available ports
/// </summary>
public int Count => Ports.Length;
}
/// <summary>
/// Request model for opening a serial port
/// </summary>
public class OpenPortRequest
{
/// <summary>
/// Name of the serial port to open (e.g., "COM1", "/dev/ttyUSB0")
/// </summary>
[Required]
public string PortName { get; set; } = string.Empty;
/// <summary>
/// Baud rate for communication (default: 9600)
/// </summary>
public int BaudRate { get; set; } = 9600;
/// <summary>
/// Parity setting (default: None)
/// </summary>
public Parity Parity { get; set; } = Parity.None;
/// <summary>
/// Stop bits setting (default: One)
/// </summary>
public StopBits StopBits { get; set; } = StopBits.One;
/// <summary>
/// Handshake setting (default: None)
/// </summary>
public Handshake Handshake { get; set; } = Handshake.None;
}
/// <summary>
/// Response model for port operations
/// </summary>
public class PortOperationResponse
{
/// <summary>
/// Indicates if the operation was successful
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Human-readable message about the operation
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// Name of the port that was operated on
/// </summary>
public string? PortName { get; set; }
/// <summary>
/// Current status of the port (open/closed)
/// </summary>
public bool IsOpen { get; set; }
}
/// <summary>
/// Request model for sending CAT commands
/// </summary>
public class SendCommandRequest
{
/// <summary>
/// CAT command string to send to the radio
/// </summary>
[Required]
public string Command { get; set; } = string.Empty;
}
/// <summary>
/// Response model for CAT command operations
/// </summary>
public class CommandResponse
{
/// <summary>
/// Indicates if the command was sent successfully
/// </summary>
public bool Success { get; set; }
/// <summary>
/// The command that was sent
/// </summary>
public string Command { get; set; } = string.Empty;
/// <summary>
/// Response received from the radio (if any)
/// </summary>
public string? Response { get; set; }
/// <summary>
/// Human-readable message about the operation
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// Timestamp when the command was executed
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Response model for errors
/// </summary>
public class ErrorResponse
{
/// <summary>
/// Error message
/// </summary>
public string Error { get; set; } = string.Empty;
/// <summary>
/// Additional details about the error
/// </summary>
public string? Details { get; set; }
/// <summary>
/// Timestamp when the error occurred
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
}

196
Client/README.md Normal file
View file

@ -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
<ProjectReference Include="path/to/SharpCAT.Client/SharpCAT.Client.csproj" />
```
## 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<SharpCATClient>(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.

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
</Project>

325
Client/SharpCATClient.cs Normal file
View file

@ -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
{
/// <summary>
/// Client for accessing SharpCAT Server API endpoints
/// </summary>
public class SharpCATClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly bool _ownsHttpClient;
/// <summary>
/// Gets the base address of the SharpCAT server
/// </summary>
public Uri BaseAddress => _httpClient.BaseAddress ?? throw new InvalidOperationException("Base address not set");
/// <summary>
/// Initializes a new instance of the SharpCATClient with the specified base address
/// </summary>
/// <param name="baseAddress">Base URL of the SharpCAT server (e.g., "http://localhost:5188")</param>
public SharpCATClient(string baseAddress) : this(new Uri(baseAddress))
{
}
/// <summary>
/// Initializes a new instance of the SharpCATClient with the specified base address
/// </summary>
/// <param name="baseAddress">Base URI of the SharpCAT server</param>
public SharpCATClient(Uri baseAddress)
{
_httpClient = new HttpClient { BaseAddress = baseAddress };
_ownsHttpClient = true;
}
/// <summary>
/// Initializes a new instance of the SharpCATClient with an existing HttpClient
/// </summary>
/// <param name="httpClient">Configured HttpClient instance</param>
public SharpCATClient(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_ownsHttpClient = false;
}
/// <summary>
/// Gets all available serial ports on the system
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>List of available serial ports</returns>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<PortListResponse> GetPortsAsync(CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient.GetFromJsonAsync<PortListResponse>("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);
}
}
/// <summary>
/// Opens and configures a serial port for communication
/// </summary>
/// <param name="request">Port configuration parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result of the port open operation</returns>
/// <exception cref="ArgumentNullException">Thrown when request is null</exception>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<PortOperationResponse> 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<PortOperationResponse>(cancellationToken: cancellationToken);
return response ?? throw new SharpCATClientException("Failed to deserialize response");
}
else
{
var errorResponse = await httpResponse.Content.ReadFromJsonAsync<ErrorResponse>(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);
}
}
/// <summary>
/// Closes the currently opened serial port
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result of the port close operation</returns>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<PortOperationResponse> ClosePortAsync(CancellationToken cancellationToken = default)
{
try
{
var httpResponse = await _httpClient.PostAsync("api/cat/close", null, cancellationToken);
if (httpResponse.IsSuccessStatusCode)
{
var response = await httpResponse.Content.ReadFromJsonAsync<PortOperationResponse>(cancellationToken: cancellationToken);
return response ?? throw new SharpCATClientException("Failed to deserialize response");
}
else
{
var errorResponse = await httpResponse.Content.ReadFromJsonAsync<ErrorResponse>(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);
}
}
/// <summary>
/// Gets the current status of the serial port connection
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Current port status</returns>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<PortOperationResponse> GetStatusAsync(CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient.GetFromJsonAsync<PortOperationResponse>("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);
}
}
/// <summary>
/// Sends a CAT command to the connected radio
/// </summary>
/// <param name="request">CAT command to send</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result of the command operation including any response from the radio</returns>
/// <exception cref="ArgumentNullException">Thrown when request is null</exception>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<CommandResponse> 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<CommandResponse>(cancellationToken: cancellationToken);
return response ?? throw new SharpCATClientException("Failed to deserialize response");
}
else
{
var errorResponse = await httpResponse.Content.ReadFromJsonAsync<ErrorResponse>(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);
}
}
/// <summary>
/// Sends a CAT command to the connected radio
/// </summary>
/// <param name="command">CAT command string to send</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result of the command operation including any response from the radio</returns>
/// <exception cref="ArgumentNullException">Thrown when command is null</exception>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<CommandResponse> 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);
}
/// <summary>
/// Gets the current frequency from the radio (VFO A)
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Current frequency in Hz, or null if the command failed or returned no data</returns>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<long?> 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;
}
/// <summary>
/// Sets the frequency for the radio (VFO A)
/// </summary>
/// <param name="frequencyHz">Frequency in Hz</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if the command was sent successfully</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when frequency is out of valid range</exception>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<bool> 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;
}
/// <summary>
/// Gets the current frequency from the radio (VFO B)
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Current frequency in Hz, or null if the command failed or returned no data</returns>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<long?> 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;
}
/// <summary>
/// Sets the frequency for the radio (VFO B)
/// </summary>
/// <param name="frequencyHz">Frequency in Hz</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if the command was sent successfully</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when frequency is out of valid range</exception>
/// <exception cref="SharpCATClientException">Thrown when the API call fails</exception>
public async Task<bool> 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;
}
/// <summary>
/// Disposes the client and releases resources
/// </summary>
public void Dispose()
{
if (_ownsHttpClient)
{
_httpClient?.Dispose();
}
}
}
}

View file

@ -0,0 +1,60 @@
using System;
namespace SharpCAT.Client
{
/// <summary>
/// Exception thrown by SharpCAT client operations
/// </summary>
public class SharpCATClientException : Exception
{
/// <summary>
/// Additional details about the error
/// </summary>
public string? Details { get; }
/// <summary>
/// Initializes a new instance of the SharpCATClientException class
/// </summary>
public SharpCATClientException()
{
}
/// <summary>
/// Initializes a new instance of the SharpCATClientException class with a specified error message
/// </summary>
/// <param name="message">The message that describes the error</param>
public SharpCATClientException(string message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the SharpCATClientException class with a specified error message and details
/// </summary>
/// <param name="message">The message that describes the error</param>
/// <param name="details">Additional details about the error</param>
public SharpCATClientException(string message, string? details) : base(message)
{
Details = details;
}
/// <summary>
/// Initializes a new instance of the SharpCATClientException class with a specified error message and a reference to the inner exception
/// </summary>
/// <param name="message">The message that describes the error</param>
/// <param name="innerException">The exception that is the cause of the current exception</param>
public SharpCATClientException(string message, Exception innerException) : base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the SharpCATClientException class with a specified error message, details, and a reference to the inner exception
/// </summary>
/// <param name="message">The message that describes the error</param>
/// <param name="details">Additional details about the error</param>
/// <param name="innerException">The exception that is the cause of the current exception</param>
public SharpCATClientException(string message, string? details, Exception innerException) : base(message, innerException)
{
Details = details;
}
}
}

View file

@ -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}