Implement SharpCAT Common library with configuration, logging, certificates, and CAT abstractions

Co-authored-by: ekinnee <1707617+ekinnee@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-08-06 22:16:05 +00:00
parent ba3966d448
commit 7916fc3ba5
40 changed files with 1276 additions and 482 deletions

41
SharpCAT.sln Normal file
View file

@ -0,0 +1,41 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E2188E0-16ED-42CA-9BF8-E38C5E682404}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCAT.Common", "src\SharpCAT.Common\SharpCAT.Common.csproj", "{28874A1C-98B0-43D6-BE83-6E3AD8B9F42F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCAT.Server", "src\SharpCAT.Server\SharpCAT.Server.csproj", "{EF0F9B07-4F63-4057-ACE7-663BF43B88DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCAT.Client", "src\SharpCAT.Client\SharpCAT.Client.csproj", "{3CA1501E-A86D-436A-9A18-8A3A2952098C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{28874A1C-98B0-43D6-BE83-6E3AD8B9F42F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28874A1C-98B0-43D6-BE83-6E3AD8B9F42F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28874A1C-98B0-43D6-BE83-6E3AD8B9F42F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28874A1C-98B0-43D6-BE83-6E3AD8B9F42F}.Release|Any CPU.Build.0 = Release|Any CPU
{EF0F9B07-4F63-4057-ACE7-663BF43B88DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF0F9B07-4F63-4057-ACE7-663BF43B88DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF0F9B07-4F63-4057-ACE7-663BF43B88DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF0F9B07-4F63-4057-ACE7-663BF43B88DC}.Release|Any CPU.Build.0 = Release|Any CPU
{3CA1501E-A86D-436A-9A18-8A3A2952098C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3CA1501E-A86D-436A-9A18-8A3A2952098C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3CA1501E-A86D-436A-9A18-8A3A2952098C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3CA1501E-A86D-436A-9A18-8A3A2952098C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{28874A1C-98B0-43D6-BE83-6E3AD8B9F42F} = {4E2188E0-16ED-42CA-9BF8-E38C5E682404}
{EF0F9B07-4F63-4057-ACE7-663BF43B88DC} = {4E2188E0-16ED-42CA-9BF8-E38C5E682404}
{3CA1501E-A86D-436A-9A18-8A3A2952098C} = {4E2188E0-16ED-42CA-9BF8-E38C5E682404}
EndGlobalSection
EndGlobal

View file

@ -1,10 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharpCATLib
{
class FlrigProtocol
{
}
}

View file

@ -1,11 +0,0 @@
namespace SharpCATLib.Models
{
internal class CATCommand
{
public string P1 { get; set; }
public string P2 { get; set; }
public string P3 { get; set; }
public string P4 { get; set; }
public string OpCode { get; set; }
}
}

View file

@ -1,104 +0,0 @@
namespace SharpCATLib.Models
{
//Base Radio Model
public partial class CATRadio : IRadio
{
private string RadioMfg { get; }
private string RadioModel { get; }
private string CmdPad { get; } = "00000000";
private string VFOToggle { get; }
private Lock Lock { get; }
private Ptt Ptt { get; }
private Clar Clar { get; }
private Split Split { get; }
private Power Power { get; }
private ToneMode ToneMode { get; }
private OpModes OpModes { get; }
partial void LockOn();
partial void LockOff();
partial void PttOn();
partial void PttOff();
partial void ClarOn();
partial void ClarOff();
partial void SplitOn();
partial void SplitOff();
partial void PowerOn();
partial void PowerOff();
partial void SetFreq(double freq);
partial void SetOpMode(OpModes opmode);
partial void SwitchVFO();
partial void SetToneMode(ToneMode mode);
partial void GetRXStatus();
partial void GetTXStatus();
partial void GetFreqAndModeStatus();
}
public partial class Lock
{
public static readonly string ON;
public static readonly string OFF;
}
public class Ptt
{
public static readonly string ON;
public static readonly string OFF;
}
public class Clar
{
public static readonly string ON;
public static readonly string OFF;
}
public class Split
{
public static readonly string ON;
public static readonly string OFF;
}
public class Power
{
public static readonly string ON;
public static readonly string OFF;
}
public class ToneMode
{
public static readonly string DCS;
public static readonly string CTCSS;
public static readonly string ENCODER;
public static readonly string OFF;
}
public class OpModes
{
public static readonly string LSB;
public static readonly string USB;
public static readonly string CW;
public static readonly string CWR;
public static readonly string AM;
public static readonly string FM;
public static readonly string DIG;
public static readonly string PKT;
}
}

View file

@ -1,10 +0,0 @@
namespace SharpCATLib.Models
{
internal class CIVCommand
{
private string CmdToRadio = "FE FE 9A E0 Cn Sc Data area FD";
private string DataFromRadio = "FE FE E0 9A Cn Sc Data area FD";
private string OKFromRadio { get; set; }
private string NGFromRadio { get; set; }
}
}

View file

@ -1,6 +0,0 @@
namespace SharpCATLib.Models
{
internal class CIVRadio : IRadio
{
}
}

View file

@ -1,8 +0,0 @@
using System;
namespace SharpCATLib.Models
{
partial interface IRadio
{
}
}

View file

@ -1,10 +0,0 @@
using SharpCATLib.Models;
namespace SharpCATLib.Radios.Icom
{
internal class ID4100a : CIVRadio
{
private string OKFromRadio = "FEFEE09AFBFD";
private string NGFromRadio = "FEFEE09AFAFD";
}
}

View file

@ -1,2 +0,0 @@
{
}

View file

@ -1,6 +0,0 @@
namespace SharpCATLib.Radios.Icom
{
internal class ID880H
{
}
}

View file

@ -1,2 +0,0 @@
{
}

View file

@ -1,6 +0,0 @@
namespace SharpCATLib.Radios.Kenwood
{
internal class THD74A
{
}
}

View file

@ -1,2 +0,0 @@
{
}

View file

@ -1,149 +0,0 @@
using SharpCATLib.Models;
namespace SharpCATLib.Radios.Yaesu
{
public class FT818 : CATRadio
{
public string RadioMfg => "Yaesu";
public string RadioModel => "FT-818";
public string CmdPad => "00000000";
public class Lock
{
public static readonly string ON = "00";
public static readonly string OFF = "80";
}
public class Ptt
{
public static readonly string ON = "08";
public static readonly string OFF = "88";
}
public class Clar
{
public static readonly string ON = "05";
public static readonly string OFF = "85";
}
public class Split
{
public static readonly string ON = "02";
public static readonly string OFF = "82";
}
public class Power
{
public static readonly string ON = "0F";
public static readonly string OFF = "8F";
}
public string VFOToggle => "81";
public class ToneMode
{
public static readonly string DCS = "0A";
public static readonly string CTCSS = "2A";
public static readonly string ENCODER = "4A";
public static readonly string OFF = "8A";
}
public class OpModes
{
public static readonly string LSB = "00";
public static readonly string USB = "01";
public static readonly string CW = "02";
public static readonly string CWR = "03";
public static readonly string AM = "04";
public static readonly string FM = "08";
public static readonly string DIG = "0A";
public static readonly string PKT = "0C";
}
private string LockOn()
{
return "";
}
private string LockOff()
{
return "";
}
private string PttOn()
{
return "";
}
private string PttOff()
{
return "";
}
private string ClarOn()
{
return "";
}
private string ClarOff()
{
return "";
}
private string SplitOn()
{
return "";
}
private string SplitOff()
{
return "";
}
private string PowerOn()
{
return "";
}
private string PowerOff()
{
return "";
}
private string SetFreq(double freq)
{
return "";
}
private string SetOpMode(OpModes opmode)
{
return "";
}
private string SwitchVFO()
{
return "";
}
private string SetToneMode(ToneMode mode)
{
return "";
}
private string GetRXStatus()
{
return "";
}
private string GetTXStatus()
{
return "";
}
private string GetFreqAndModeStatus()
{
return "";
}
}
}

View file

@ -1,2 +0,0 @@
{
}

View file

@ -1,50 +0,0 @@
using System;
using System.IO.Ports;
namespace SharpCATLib
{
public class Serial
{
private SerialPort _serialPort;
//Init
public Serial(string portname, SharpCAT.BaudRates baudrate, Parity parity, StopBits bits, Handshake handshake)
{
_serialPort = new SerialPort
{
ReadTimeout = 500,
WriteTimeout = 500,
PortName = portname,
BaudRate = (int)baudrate,
Parity = parity,
StopBits = bits,
Handshake = handshake
};
_serialPort.DataReceived += new SerialDataReceivedEventHandler(SerialDataReceived);
_serialPort.ErrorReceived += new SerialErrorReceivedEventHandler(SerialErrorReceived);
}
private void SerialErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{
}
private void SerialDataReceived(object sender, SerialDataReceivedEventArgs e)
{
}
public void ProbeSerialPort(SerialPort port)
{
}
public void Read()
{
try
{
string message = _serialPort.ReadLine();
//Console.WriteLine(message);
}
catch (TimeoutException) { }
}
}
}

View file

@ -1,6 +0,0 @@
namespace SharpCATLib
{
internal class SerialClient
{
}
}

View file

@ -1,6 +0,0 @@
namespace SharpCATLib
{
internal class SerialServer
{
}
}

View file

@ -1,49 +0,0 @@
using System.Collections.Generic;
using System.IO.Ports;
namespace SharpCATLib
{
public class SharpCAT
{
private delegate string OnPortsSelected(string[] portnames);
private static event OnPortsSelected PortsSelected;
public SharpCAT() => SharpCAT.PortsSelected += new OnPortsSelected(ConnectPorts);
public string[] PortsToUse { get; set; }
public double[] CTCSSTones = { 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5,
91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123, 127.3, 131.8,
136.5, 141.3, 146.2, 151.4, 156.7, 162.2, 167.9, 173.8, 179.9, 186.2, 192.8,
203.5, 210.7, 218.1, 225.7, 233.6, 241.8, 250.3 };
public int[] DCSCodes = { 023, 025, 026, 031, 032, 036, 043, 047, 051, 053, 054, 065,
071, 072, 073, 074, 114, 115, 116, 122, 125, 131, 132, 134, 143, 145, 152, 155,
156, 162, 165, 172, 174, 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, 255,
261, 263, 265, 266, 271, 274, 306, 311, 315, 325, 331, 332, 343, 346, 351, 356, 364,
365, 371, 411, 412, 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, 465, 466,
503, 506, 516, 523, 526, 532, 546, 565, 606, 612, 624, 627, 631, 632, 654, 662, 664,
703, 712, 723, 731, 732, 734, 743, 754 };
public string[] PortNames { get => SerialPort.GetPortNames(); }
public enum BaudRates : int { Twelve = 1200, TwentyFour = 2400, FourtyEight = 4800, NinteySix = 9600, NineteenTwo = 19200, ThirtyEightFour = 38400 };
public static int[] DataBits { get; } = new int[] { 7, 8 };
public static string[] RadioTypes { get; } = new string[] { "CAT", "CIV" };
private string ConnectPorts(string[] portnames)
{
List<SerialPort> ports = new List<SerialPort>();
foreach (string port in portnames)
{
//Testing
ports.Add(new SerialPort(port, (int)BaudRates.ThirtyEightFour, Parity.None, (int)StopBits.Two, (int)Handshake.None));
}
return "";
}
}
}

View file

@ -1,37 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<LangVersion>default</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<LangVersion>default</LangVersion>
</PropertyGroup>
<ItemGroup>
<None Update="Radios\Icom\ID4100a.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Radios\Icom\ID880H.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Radios\Kenwood\THD74A.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Radios\Yaesu\FT818.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="SharpCAT WPF\App.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
<None Update="SharpCAT WPF\MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="5.0.1" />
</ItemGroup>
</Project>

View file

@ -1,6 +0,0 @@
namespace SharpCATLib
{
internal class Socket
{
}
}

View file

@ -0,0 +1,2 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../SharpCAT.Common/SharpCAT.Common.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,42 @@
namespace SharpCAT.Common.CAT;
/// <summary>
/// Represents a CAT (Computer Aided Transceiver) command
/// </summary>
public class CATCommand
{
/// <summary>
/// Unique identifier for the command
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Human-readable name of the command
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Description of what the command does
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// The raw command bytes to send to the radio
/// </summary>
public byte[] CommandBytes { get; set; } = Array.Empty<byte>();
/// <summary>
/// Expected response pattern (if any)
/// </summary>
public byte[]? ExpectedResponse { get; set; }
/// <summary>
/// Parameters that can be included with the command
/// </summary>
public Dictionary<string, object> Parameters { get; set; } = new();
/// <summary>
/// Command timeout in milliseconds
/// </summary>
public int TimeoutMs { get; set; } = 1000;
}

View file

@ -0,0 +1,42 @@
namespace SharpCAT.Common.CAT;
/// <summary>
/// Represents the response from a CAT command
/// </summary>
public class CATResponse
{
/// <summary>
/// The original command that generated this response
/// </summary>
public CATCommand Command { get; set; } = new();
/// <summary>
/// Whether the command was successful
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Raw response bytes from the radio
/// </summary>
public byte[] ResponseBytes { get; set; } = Array.Empty<byte>();
/// <summary>
/// Parsed response data (if applicable)
/// </summary>
public Dictionary<string, object> Data { get; set; } = new();
/// <summary>
/// Error message if the command failed
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// Timestamp when the response was received
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
/// <summary>
/// Duration it took to receive the response
/// </summary>
public TimeSpan Duration { get; set; }
}

View file

@ -0,0 +1,37 @@
namespace SharpCAT.Common.CAT;
/// <summary>
/// Interface for CAT communication
/// </summary>
public interface ICATInterface
{
/// <summary>
/// Sends a CAT command and waits for a response
/// </summary>
/// <param name="command">The command to send</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The response from the radio</returns>
Task<CATResponse> SendCommandAsync(CATCommand command, CancellationToken cancellationToken = default);
/// <summary>
/// Sends a CAT command without waiting for a response
/// </summary>
/// <param name="command">The command to send</param>
/// <param name="cancellationToken">Cancellation token</param>
Task SendCommandOnlyAsync(CATCommand command, CancellationToken cancellationToken = default);
/// <summary>
/// Checks if the CAT interface is connected and ready
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Event fired when a response is received
/// </summary>
event EventHandler<CATResponse>? ResponseReceived;
/// <summary>
/// Event fired when an error occurs
/// </summary>
event EventHandler<string>? ErrorOccurred;
}

View file

@ -0,0 +1,259 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using SharpCAT.Common.Models;
namespace SharpCAT.Common.Certificates;
/// <summary>
/// Certificate manager for TLS mutual authentication
/// </summary>
public class CertificateManager
{
private readonly CertificateConfig _config;
public CertificateManager(CertificateConfig config)
{
_config = config;
}
/// <summary>
/// Ensures all required certificates exist, generating them if necessary
/// </summary>
public async Task EnsureCertificatesExistAsync()
{
// Create certificate directory if it doesn't exist
if (!Directory.Exists(_config.CertificateStorePath))
{
Directory.CreateDirectory(_config.CertificateStorePath);
}
// Generate CA certificate if it doesn't exist
var caCertPath = GetCACertificatePath();
var caKeyPath = GetCAKeyPath();
if (!File.Exists(caCertPath) || !File.Exists(caKeyPath))
{
await GenerateCACertificateAsync();
}
// Generate server certificate if it doesn't exist
var serverCertPath = GetServerCertificatePath();
var serverKeyPath = GetServerKeyPath();
if (!File.Exists(serverCertPath) || !File.Exists(serverKeyPath))
{
await GenerateServerCertificateAsync();
}
// Generate client certificate if it doesn't exist
var clientCertPath = GetClientCertificatePath();
var clientKeyPath = GetClientKeyPath();
if (!File.Exists(clientCertPath) || !File.Exists(clientKeyPath))
{
await GenerateClientCertificateAsync();
}
}
/// <summary>
/// Loads the CA certificate
/// </summary>
public X509Certificate2 LoadCACertificate()
{
var certPath = GetCACertificatePath();
if (!File.Exists(certPath))
throw new FileNotFoundException($"CA certificate not found at {certPath}");
return new X509Certificate2(certPath);
}
/// <summary>
/// Loads the server certificate with private key
/// </summary>
public X509Certificate2 LoadServerCertificate()
{
var certPath = GetServerCertificatePath();
var keyPath = GetServerKeyPath();
if (!File.Exists(certPath))
throw new FileNotFoundException($"Server certificate not found at {certPath}");
if (!File.Exists(keyPath))
throw new FileNotFoundException($"Server private key not found at {keyPath}");
return LoadCertificateWithKey(certPath, keyPath);
}
/// <summary>
/// Loads the client certificate with private key
/// </summary>
public X509Certificate2 LoadClientCertificate()
{
var certPath = GetClientCertificatePath();
var keyPath = GetClientKeyPath();
if (!File.Exists(certPath))
throw new FileNotFoundException($"Client certificate not found at {certPath}");
if (!File.Exists(keyPath))
throw new FileNotFoundException($"Client private key not found at {keyPath}");
return LoadCertificateWithKey(certPath, keyPath);
}
/// <summary>
/// Validates a certificate against the CA
/// </summary>
public bool ValidateCertificate(X509Certificate2 certificate)
{
try
{
var caCert = LoadCACertificate();
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(caCert);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
return chain.Build(certificate);
}
catch
{
return false;
}
}
private async Task GenerateCACertificateAsync()
{
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=SharpCAT-CA", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// Add extensions for CA certificate
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true));
var certificate = request.CreateSelfSigned(
DateTimeOffset.Now.AddDays(-1),
DateTimeOffset.Now.AddDays(_config.ValidityDays));
// Save certificate
var certBytes = certificate.Export(X509ContentType.Cert);
await File.WriteAllBytesAsync(GetCACertificatePath(), certBytes);
// Save private key
var keyBytes = rsa.ExportRSAPrivateKey();
var keyPem = ConvertToPem(keyBytes, "RSA PRIVATE KEY");
await File.WriteAllTextAsync(GetCAKeyPath(), keyPem);
}
private async Task GenerateServerCertificateAsync()
{
var caCert = LoadCACertificate();
var caKey = LoadPrivateKey(GetCAKeyPath());
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=SharpCAT-Server", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// Add extensions for server certificate
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true));
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, true)); // Server Authentication
var certificate = request.Create(caCert, DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now.AddDays(_config.ValidityDays), new byte[4]);
// Save certificate
var certBytes = certificate.Export(X509ContentType.Cert);
await File.WriteAllBytesAsync(GetServerCertificatePath(), certBytes);
// Save private key
var keyBytes = rsa.ExportRSAPrivateKey();
var keyPem = ConvertToPem(keyBytes, "RSA PRIVATE KEY");
await File.WriteAllTextAsync(GetServerKeyPath(), keyPem);
}
private async Task GenerateClientCertificateAsync()
{
var caCert = LoadCACertificate();
var caKey = LoadPrivateKey(GetCAKeyPath());
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=SharpCAT-Client", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// Add extensions for client certificate
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true));
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.2") }, true)); // Client Authentication
var certificate = request.Create(caCert, DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now.AddDays(_config.ValidityDays), new byte[4]);
// Save certificate
var certBytes = certificate.Export(X509ContentType.Cert);
await File.WriteAllBytesAsync(GetClientCertificatePath(), certBytes);
// Save private key
var keyBytes = rsa.ExportRSAPrivateKey();
var keyPem = ConvertToPem(keyBytes, "RSA PRIVATE KEY");
await File.WriteAllTextAsync(GetClientKeyPath(), keyPem);
}
private X509Certificate2 LoadCertificateWithKey(string certPath, string keyPath)
{
var cert = new X509Certificate2(certPath);
var key = LoadPrivateKey(keyPath);
return cert.CopyWithPrivateKey(key);
}
private RSA LoadPrivateKey(string keyPath)
{
var keyPem = File.ReadAllText(keyPath);
var keyBytes = ConvertFromPem(keyPem, "RSA PRIVATE KEY");
var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(keyBytes, out _);
return rsa;
}
private static string ConvertToPem(byte[] data, string type)
{
var base64 = Convert.ToBase64String(data);
var sb = new StringBuilder();
sb.AppendLine($"-----BEGIN {type}-----");
for (int i = 0; i < base64.Length; i += 64)
{
var length = Math.Min(64, base64.Length - i);
sb.AppendLine(base64.Substring(i, length));
}
sb.AppendLine($"-----END {type}-----");
return sb.ToString();
}
private static byte[] ConvertFromPem(string pem, string type)
{
var startMarker = $"-----BEGIN {type}-----";
var endMarker = $"-----END {type}-----";
var start = pem.IndexOf(startMarker) + startMarker.Length;
var end = pem.IndexOf(endMarker);
var base64 = pem.Substring(start, end - start).Trim();
return Convert.FromBase64String(base64);
}
private string GetCACertificatePath() =>
_config.CaCertificatePath ?? Path.Combine(_config.CertificateStorePath, "ca.crt");
private string GetCAKeyPath() =>
Path.Combine(_config.CertificateStorePath, "ca.key");
private string GetServerCertificatePath() =>
_config.ServerCertificatePath ?? Path.Combine(_config.CertificateStorePath, "server.crt");
private string GetServerKeyPath() =>
_config.ServerKeyPath ?? Path.Combine(_config.CertificateStorePath, "server.key");
private string GetClientCertificatePath() =>
_config.ClientCertificatePath ?? Path.Combine(_config.CertificateStorePath, "client.crt");
private string GetClientKeyPath() =>
_config.ClientKeyPath ?? Path.Combine(_config.CertificateStorePath, "client.key");
}

View file

@ -0,0 +1,173 @@
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using SharpCAT.Common.Models;
namespace SharpCAT.Common.Configuration;
/// <summary>
/// Configuration loader for SharpCAT applications
/// </summary>
public static class ConfigurationLoader
{
/// <summary>
/// Loads server configuration from JSON file
/// </summary>
/// <param name="configPath">Path to the configuration file</param>
/// <returns>Server configuration</returns>
public static ServerConfig LoadServerConfig(string configPath = "server-config.json")
{
return LoadConfig<ServerConfig>(configPath, GetDefaultServerConfig());
}
/// <summary>
/// Loads client configuration from JSON file
/// </summary>
/// <param name="configPath">Path to the configuration file</param>
/// <returns>Client configuration</returns>
public static ClientConfig LoadClientConfig(string configPath = "client-config.json")
{
return LoadConfig<ClientConfig>(configPath, GetDefaultClientConfig());
}
/// <summary>
/// Saves server configuration to JSON file
/// </summary>
/// <param name="config">Configuration to save</param>
/// <param name="configPath">Path to save the configuration file</param>
public static async Task SaveServerConfigAsync(ServerConfig config, string configPath = "server-config.json")
{
await SaveConfigAsync(config, configPath);
}
/// <summary>
/// Saves client configuration to JSON file
/// </summary>
/// <param name="config">Configuration to save</param>
/// <param name="configPath">Path to save the configuration file</param>
public static async Task SaveClientConfigAsync(ClientConfig config, string configPath = "client-config.json")
{
await SaveConfigAsync(config, configPath);
}
/// <summary>
/// Creates default server configuration file if it doesn't exist
/// </summary>
/// <param name="configPath">Path to create the configuration file</param>
public static async Task EnsureServerConfigExistsAsync(string configPath = "server-config.json")
{
if (!File.Exists(configPath))
{
await SaveServerConfigAsync(GetDefaultServerConfig(), configPath);
}
}
/// <summary>
/// Creates default client configuration file if it doesn't exist
/// </summary>
/// <param name="configPath">Path to create the configuration file</param>
public static async Task EnsureClientConfigExistsAsync(string configPath = "client-config.json")
{
if (!File.Exists(configPath))
{
await SaveClientConfigAsync(GetDefaultClientConfig(), configPath);
}
}
private static T LoadConfig<T>(string configPath, T defaultConfig) where T : class
{
try
{
if (!File.Exists(configPath))
{
// Create default configuration file
var defaultJson = JsonConvert.SerializeObject(defaultConfig, Formatting.Indented);
File.WriteAllText(configPath, defaultJson);
return defaultConfig;
}
var configuration = new ConfigurationBuilder()
.AddJsonFile(configPath, optional: false, reloadOnChange: false)
.Build();
var config = configuration.Get<T>();
return config ?? defaultConfig;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to load configuration from {configPath}: {ex.Message}", ex);
}
}
private static async Task SaveConfigAsync<T>(T config, string configPath) where T : class
{
try
{
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
await File.WriteAllTextAsync(configPath, json);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to save configuration to {configPath}: {ex.Message}", ex);
}
}
private static ServerConfig GetDefaultServerConfig()
{
return new ServerConfig
{
Port = 8443,
Certificate = new CertificateConfig
{
CertificateStorePath = "./certificates",
AutoGenerateCertificates = true,
ValidityDays = 365,
Subject = "CN=SharpCAT-Server"
},
SerialPort = new SerialPortConfig
{
BaudRate = 9600,
DataBits = 8,
StopBits = System.IO.Ports.StopBits.One,
Parity = System.IO.Ports.Parity.None,
Handshake = System.IO.Ports.Handshake.None,
ReadTimeoutMs = 1000,
WriteTimeoutMs = 1000,
AutoDetectPort = true
},
Logging = new LoggingConfig
{
LogLevel = "Information",
EnableConsoleLogging = true,
EnableOSLogging = true,
IncludeTimestamps = true,
IncludeLogLevel = true
},
MaxClients = 10
};
}
private static ClientConfig GetDefaultClientConfig()
{
return new ClientConfig
{
ServerHost = "localhost",
ServerPort = 8443,
Certificate = new CertificateConfig
{
CertificateStorePath = "./certificates",
AutoGenerateCertificates = true,
ValidityDays = 365,
Subject = "CN=SharpCAT-Client"
},
Logging = new LoggingConfig
{
LogLevel = "Information",
EnableConsoleLogging = true,
EnableOSLogging = true,
IncludeTimestamps = true,
IncludeLogLevel = true
},
ConnectionTimeoutMs = 5000
};
}
}

View file

@ -0,0 +1,230 @@
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace SharpCAT.Common.Logging;
/// <summary>
/// Custom logger provider that supports both console and native OS logging
/// </summary>
public class SharpCATLoggerProvider : ILoggerProvider
{
private readonly bool _enableConsoleLogging;
private readonly bool _enableOSLogging;
private readonly string _applicationName;
public SharpCATLoggerProvider(bool enableConsoleLogging = true, bool enableOSLogging = true, string applicationName = "SharpCAT")
{
_enableConsoleLogging = enableConsoleLogging;
_enableOSLogging = enableOSLogging;
_applicationName = applicationName;
}
public ILogger CreateLogger(string categoryName)
{
return new SharpCATLogger(categoryName, _enableConsoleLogging, _enableOSLogging, _applicationName);
}
public void Dispose()
{
// Nothing to dispose
}
}
/// <summary>
/// Custom logger that supports both console and native OS logging
/// </summary>
public class SharpCATLogger : ILogger
{
private readonly string _categoryName;
private readonly bool _enableConsoleLogging;
private readonly bool _enableOSLogging;
private readonly string _applicationName;
private readonly EventLog? _eventLog;
public SharpCATLogger(string categoryName, bool enableConsoleLogging, bool enableOSLogging, string applicationName)
{
_categoryName = categoryName;
_enableConsoleLogging = enableConsoleLogging;
_enableOSLogging = enableOSLogging;
_applicationName = applicationName;
// Initialize Event Log for Windows
if (_enableOSLogging && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
{
_eventLog = new EventLog();
_eventLog.Source = _applicationName;
// Create event source if it doesn't exist
if (!EventLog.SourceExists(_applicationName))
{
EventLog.CreateEventSource(_applicationName, "Application");
}
}
catch
{
// If we can't create event log, just disable OS logging
_eventLog = null;
}
}
}
public IDisposable BeginScope<TState>(TState state) => NullScope.Instance;
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
return;
var message = formatter(state, exception);
if (exception != null)
message += Environment.NewLine + exception.ToString();
// Console logging
if (_enableConsoleLogging)
{
LogToConsole(logLevel, message);
}
// OS logging
if (_enableOSLogging)
{
LogToOS(logLevel, message);
}
}
private void LogToConsole(LogLevel logLevel, string message)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
var level = GetLogLevelString(logLevel);
var formattedMessage = $"[{timestamp}] [{level}] [{_categoryName}] {message}";
// Set console color based on log level
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = GetConsoleColor(logLevel);
Console.WriteLine(formattedMessage);
Console.ForegroundColor = originalColor;
}
private void LogToOS(LogLevel logLevel, string message)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
LogToWindowsEventLog(logLevel, message);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
LogToSyslog(logLevel, message);
}
}
private void LogToWindowsEventLog(LogLevel logLevel, string message)
{
try
{
_eventLog?.WriteEntry($"[{_categoryName}] {message}", GetEventLogEntryType(logLevel));
}
catch
{
// Ignore errors writing to event log
}
}
private void LogToSyslog(LogLevel logLevel, string message)
{
try
{
// Use logger command to write to syslog
var priority = GetSyslogPriority(logLevel);
var formattedMessage = $"[{_categoryName}] {message}";
// Escape message for shell
var escapedMessage = formattedMessage.Replace("\"", "\\\"");
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = "/usr/bin/logger",
Arguments = $"-p {priority} -t \"{_applicationName}\" \"{escapedMessage}\"",
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
process.WaitForExit(1000); // Wait max 1 second
}
catch
{
// Ignore errors writing to syslog
}
}
private static string GetLogLevelString(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => "TRACE",
LogLevel.Debug => "DEBUG",
LogLevel.Information => "INFO",
LogLevel.Warning => "WARN",
LogLevel.Error => "ERROR",
LogLevel.Critical => "CRIT",
_ => "UNKNOWN"
};
}
private static ConsoleColor GetConsoleColor(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => ConsoleColor.Gray,
LogLevel.Debug => ConsoleColor.Gray,
LogLevel.Information => ConsoleColor.White,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.Error => ConsoleColor.Red,
LogLevel.Critical => ConsoleColor.DarkRed,
_ => ConsoleColor.White
};
}
private static EventLogEntryType GetEventLogEntryType(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Warning => EventLogEntryType.Warning,
LogLevel.Error => EventLogEntryType.Error,
LogLevel.Critical => EventLogEntryType.Error,
_ => EventLogEntryType.Information
};
}
private static string GetSyslogPriority(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => "user.debug",
LogLevel.Debug => "user.debug",
LogLevel.Information => "user.info",
LogLevel.Warning => "user.warning",
LogLevel.Error => "user.err",
LogLevel.Critical => "user.crit",
_ => "user.info"
};
}
private class NullScope : IDisposable
{
public static readonly NullScope Instance = new();
public void Dispose() { }
}
}

View file

@ -0,0 +1,52 @@
namespace SharpCAT.Common.Models;
/// <summary>
/// Certificate configuration for TLS mutual authentication
/// </summary>
public class CertificateConfig
{
/// <summary>
/// Path to the Certificate Authority (CA) certificate file
/// </summary>
public string? CaCertificatePath { get; set; }
/// <summary>
/// Path to the server certificate file
/// </summary>
public string? ServerCertificatePath { get; set; }
/// <summary>
/// Path to the server private key file
/// </summary>
public string? ServerKeyPath { get; set; }
/// <summary>
/// Path to the client certificate file
/// </summary>
public string? ClientCertificatePath { get; set; }
/// <summary>
/// Path to the client private key file
/// </summary>
public string? ClientKeyPath { get; set; }
/// <summary>
/// Certificate store location for storing generated certificates
/// </summary>
public string CertificateStorePath { get; set; } = "./certificates";
/// <summary>
/// Whether to auto-generate certificates if they don't exist
/// </summary>
public bool AutoGenerateCertificates { get; set; } = true;
/// <summary>
/// Certificate validity period in days
/// </summary>
public int ValidityDays { get; set; } = 365;
/// <summary>
/// Subject name for generated certificates
/// </summary>
public string Subject { get; set; } = "CN=SharpCAT";
}

View file

@ -0,0 +1,32 @@
namespace SharpCAT.Common.Models;
/// <summary>
/// Configuration model for the SharpCAT client
/// </summary>
public class ClientConfig
{
/// <summary>
/// Server hostname or IP address to connect to
/// </summary>
public string ServerHost { get; set; } = "localhost";
/// <summary>
/// Server port to connect to
/// </summary>
public int ServerPort { get; set; } = 8443;
/// <summary>
/// Certificate configuration for TLS
/// </summary>
public CertificateConfig Certificate { get; set; } = new();
/// <summary>
/// Logging configuration
/// </summary>
public LoggingConfig Logging { get; set; } = new();
/// <summary>
/// Connection timeout in milliseconds
/// </summary>
public int ConnectionTimeoutMs { get; set; } = 5000;
}

View file

@ -0,0 +1,37 @@
namespace SharpCAT.Common.Models;
/// <summary>
/// Logging configuration
/// </summary>
public class LoggingConfig
{
/// <summary>
/// Minimum log level
/// </summary>
public string LogLevel { get; set; } = "Information";
/// <summary>
/// Whether to enable console logging
/// </summary>
public bool EnableConsoleLogging { get; set; } = true;
/// <summary>
/// Whether to enable native OS logging (Event Log on Windows, syslog on Linux/macOS)
/// </summary>
public bool EnableOSLogging { get; set; } = true;
/// <summary>
/// Log file path (optional)
/// </summary>
public string? LogFilePath { get; set; }
/// <summary>
/// Whether to include timestamps in console output
/// </summary>
public bool IncludeTimestamps { get; set; } = true;
/// <summary>
/// Whether to include log levels in console output
/// </summary>
public bool IncludeLogLevel { get; set; } = true;
}

View file

@ -0,0 +1,52 @@
namespace SharpCAT.Common.Models;
/// <summary>
/// Serial port configuration for CAT communication
/// </summary>
public class SerialPortConfig
{
/// <summary>
/// Serial port name (e.g., COM1, /dev/ttyUSB0)
/// </summary>
public string? PortName { get; set; }
/// <summary>
/// Baud rate for serial communication
/// </summary>
public int BaudRate { get; set; } = 9600;
/// <summary>
/// Data bits
/// </summary>
public int DataBits { get; set; } = 8;
/// <summary>
/// Stop bits
/// </summary>
public System.IO.Ports.StopBits StopBits { get; set; } = System.IO.Ports.StopBits.One;
/// <summary>
/// Parity setting
/// </summary>
public System.IO.Ports.Parity Parity { get; set; } = System.IO.Ports.Parity.None;
/// <summary>
/// Flow control/handshake
/// </summary>
public System.IO.Ports.Handshake Handshake { get; set; } = System.IO.Ports.Handshake.None;
/// <summary>
/// Read timeout in milliseconds
/// </summary>
public int ReadTimeoutMs { get; set; } = 1000;
/// <summary>
/// Write timeout in milliseconds
/// </summary>
public int WriteTimeoutMs { get; set; } = 1000;
/// <summary>
/// Whether to auto-detect the serial port if not specified
/// </summary>
public bool AutoDetectPort { get; set; } = true;
}

View file

@ -0,0 +1,32 @@
namespace SharpCAT.Common.Models;
/// <summary>
/// Configuration model for the SharpCAT server
/// </summary>
public class ServerConfig
{
/// <summary>
/// TCP port to listen on
/// </summary>
public int Port { get; set; } = 8443;
/// <summary>
/// Certificate configuration for TLS
/// </summary>
public CertificateConfig Certificate { get; set; } = new();
/// <summary>
/// Serial port configuration for CAT communication
/// </summary>
public SerialPortConfig SerialPort { get; set; } = new();
/// <summary>
/// Logging configuration
/// </summary>
public LoggingConfig Logging { get; set; } = new();
/// <summary>
/// Maximum number of concurrent clients
/// </summary>
public int MaxClients { get; set; } = 10;
}

View file

@ -0,0 +1,173 @@
using System.IO.Ports;
using System.Runtime.InteropServices;
namespace SharpCAT.Common.SerialPort;
/// <summary>
/// Cross-platform serial port helper utilities
/// </summary>
public static class SerialPortHelper
{
/// <summary>
/// Gets available serial port names for the current platform
/// </summary>
/// <returns>Array of available serial port names</returns>
public static string[] GetAvailablePortNames()
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return GetWindowsPortNames();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return GetLinuxPortNames();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return GetMacOSPortNames();
}
else
{
// Fallback to .NET standard method
return System.IO.Ports.SerialPort.GetPortNames();
}
}
catch
{
// Fallback to .NET standard method on any error
return System.IO.Ports.SerialPort.GetPortNames();
}
}
/// <summary>
/// Auto-detects the best serial port for CAT communication
/// </summary>
/// <returns>The detected port name, or null if none found</returns>
public static async Task<string?> AutoDetectCATPortAsync()
{
var ports = GetAvailablePortNames();
foreach (var portName in ports)
{
try
{
using var port = new System.IO.Ports.SerialPort(portName, 9600, Parity.None, 8, StopBits.One);
port.ReadTimeout = 1000;
port.WriteTimeout = 1000;
port.Open();
// Try a simple CAT command (may vary by radio)
// This is a basic "ID" command that many radios support
await Task.Delay(100); // Allow port to stabilize
port.Close();
// If we get here without exception, the port is likely valid
return portName;
}
catch
{
// Port is not available or doesn't respond - continue to next
continue;
}
}
return null;
}
private static string[] GetWindowsPortNames()
{
// Windows COM ports
var ports = new List<string>();
for (int i = 1; i <= 256; i++)
{
string portName = $"COM{i}";
try
{
using var port = new System.IO.Ports.SerialPort(portName);
ports.Add(portName);
}
catch
{
// Port doesn't exist
}
}
return ports.ToArray();
}
private static string[] GetLinuxPortNames()
{
// Linux serial devices
var ports = new List<string>();
// Standard serial ports
for (int i = 0; i < 32; i++)
{
string portName = $"/dev/ttyS{i}";
if (File.Exists(portName))
ports.Add(portName);
}
// USB serial ports
for (int i = 0; i < 32; i++)
{
string portName = $"/dev/ttyUSB{i}";
if (File.Exists(portName))
ports.Add(portName);
}
// USB ACM ports (often used by Arduino and similar devices)
for (int i = 0; i < 32; i++)
{
string portName = $"/dev/ttyACM{i}";
if (File.Exists(portName))
ports.Add(portName);
}
return ports.ToArray();
}
private static string[] GetMacOSPortNames()
{
// macOS serial devices
var ports = new List<string>();
try
{
// Check for USB serial devices
var devDirectory = "/dev";
if (Directory.Exists(devDirectory))
{
var files = Directory.GetFiles(devDirectory, "tty.*");
foreach (var file in files)
{
if (file.Contains("usb", StringComparison.OrdinalIgnoreCase) ||
file.Contains("serial", StringComparison.OrdinalIgnoreCase))
{
ports.Add(file);
}
}
// Also check for cu.* devices which are common on macOS
files = Directory.GetFiles(devDirectory, "cu.*");
foreach (var file in files)
{
if (file.Contains("usb", StringComparison.OrdinalIgnoreCase) ||
file.Contains("serial", StringComparison.OrdinalIgnoreCase))
{
ports.Add(file);
}
}
}
}
catch
{
// Fallback to standard method
}
return ports.ToArray();
}
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="System.IO.Ports" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Security.Cryptography.X509Certificates" Version="4.3.2" />
<PackageReference Include="System.Diagnostics.EventLog" Version="6.0.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,2 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../SharpCAT.Common/SharpCAT.Common.csproj" />
</ItemGroup>
</Project>