diff --git a/SharpCAT.sln b/SharpCAT.sln new file mode 100644 index 0000000..e004bea --- /dev/null +++ b/SharpCAT.sln @@ -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 diff --git a/SharpCAT/Docs/Radios/Icom/ID4100a/advanced-manual.pdf b/SharpCAT/Docs/Radios/Icom/ID4100a/advanced-manual.pdf deleted file mode 100644 index 26336ae..0000000 Binary files a/SharpCAT/Docs/Radios/Icom/ID4100a/advanced-manual.pdf and /dev/null differ diff --git a/SharpCAT/Docs/Radios/Yaesu/FT818/Yaesu-FT-818nd-manual-FT818.pdf b/SharpCAT/Docs/Radios/Yaesu/FT818/Yaesu-FT-818nd-manual-FT818.pdf deleted file mode 100644 index 56ac168..0000000 Binary files a/SharpCAT/Docs/Radios/Yaesu/FT818/Yaesu-FT-818nd-manual-FT818.pdf and /dev/null differ diff --git a/SharpCAT/FlrigProtocol.cs b/SharpCAT/FlrigProtocol.cs deleted file mode 100644 index 75c1f94..0000000 --- a/SharpCAT/FlrigProtocol.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace SharpCATLib -{ - class FlrigProtocol - { - } -} diff --git a/SharpCAT/Models/CATCommand.cs b/SharpCAT/Models/CATCommand.cs deleted file mode 100644 index a618e6c..0000000 --- a/SharpCAT/Models/CATCommand.cs +++ /dev/null @@ -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; } - } -} \ No newline at end of file diff --git a/SharpCAT/Models/CATRadio.cs b/SharpCAT/Models/CATRadio.cs deleted file mode 100644 index b6c06a0..0000000 --- a/SharpCAT/Models/CATRadio.cs +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/SharpCAT/Models/CIVCommand.cs b/SharpCAT/Models/CIVCommand.cs deleted file mode 100644 index 5c02fd5..0000000 --- a/SharpCAT/Models/CIVCommand.cs +++ /dev/null @@ -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; } - } -} \ No newline at end of file diff --git a/SharpCAT/Models/CIVRadio.cs b/SharpCAT/Models/CIVRadio.cs deleted file mode 100644 index e1f359c..0000000 --- a/SharpCAT/Models/CIVRadio.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpCATLib.Models -{ - internal class CIVRadio : IRadio - { - } -} \ No newline at end of file diff --git a/SharpCAT/Models/IRadio.cs b/SharpCAT/Models/IRadio.cs deleted file mode 100644 index 3721fd9..0000000 --- a/SharpCAT/Models/IRadio.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; -namespace SharpCATLib.Models -{ - partial interface IRadio - { - - } -} diff --git a/SharpCAT/Radios/Icom/ID4100a.cs b/SharpCAT/Radios/Icom/ID4100a.cs deleted file mode 100644 index 95d21c3..0000000 --- a/SharpCAT/Radios/Icom/ID4100a.cs +++ /dev/null @@ -1,10 +0,0 @@ -using SharpCATLib.Models; - -namespace SharpCATLib.Radios.Icom -{ - internal class ID4100a : CIVRadio - { - private string OKFromRadio = "FEFEE09AFBFD"; - private string NGFromRadio = "FEFEE09AFAFD"; - } -} \ No newline at end of file diff --git a/SharpCAT/Radios/Icom/ID4100a.json b/SharpCAT/Radios/Icom/ID4100a.json deleted file mode 100644 index 7a73a41..0000000 --- a/SharpCAT/Radios/Icom/ID4100a.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/SharpCAT/Radios/Icom/ID880H.cs b/SharpCAT/Radios/Icom/ID880H.cs deleted file mode 100644 index 4bcbb03..0000000 --- a/SharpCAT/Radios/Icom/ID880H.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpCATLib.Radios.Icom -{ - internal class ID880H - { - } -} \ No newline at end of file diff --git a/SharpCAT/Radios/Icom/ID880H.json b/SharpCAT/Radios/Icom/ID880H.json deleted file mode 100644 index 7a73a41..0000000 --- a/SharpCAT/Radios/Icom/ID880H.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/SharpCAT/Radios/Kenwood/THD74A.cs b/SharpCAT/Radios/Kenwood/THD74A.cs deleted file mode 100644 index 5711d9d..0000000 --- a/SharpCAT/Radios/Kenwood/THD74A.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpCATLib.Radios.Kenwood -{ - internal class THD74A - { - } -} \ No newline at end of file diff --git a/SharpCAT/Radios/Kenwood/THD74A.json b/SharpCAT/Radios/Kenwood/THD74A.json deleted file mode 100644 index 7a73a41..0000000 --- a/SharpCAT/Radios/Kenwood/THD74A.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/SharpCAT/Radios/Yaesu/FT818.cs b/SharpCAT/Radios/Yaesu/FT818.cs deleted file mode 100644 index 9f7cd3f..0000000 --- a/SharpCAT/Radios/Yaesu/FT818.cs +++ /dev/null @@ -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 ""; - } - } -} \ No newline at end of file diff --git a/SharpCAT/Radios/Yaesu/FT818.json b/SharpCAT/Radios/Yaesu/FT818.json deleted file mode 100644 index 7a73a41..0000000 --- a/SharpCAT/Radios/Yaesu/FT818.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/SharpCAT/Serial.cs b/SharpCAT/Serial.cs deleted file mode 100644 index 8b39baa..0000000 --- a/SharpCAT/Serial.cs +++ /dev/null @@ -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) { } - } - } -} \ No newline at end of file diff --git a/SharpCAT/SerialClient.cs b/SharpCAT/SerialClient.cs deleted file mode 100644 index 6990f42..0000000 --- a/SharpCAT/SerialClient.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpCATLib -{ - internal class SerialClient - { - } -} \ No newline at end of file diff --git a/SharpCAT/SerialServer.cs b/SharpCAT/SerialServer.cs deleted file mode 100644 index 8e8fc9e..0000000 --- a/SharpCAT/SerialServer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpCATLib -{ - internal class SerialServer - { - } -} \ No newline at end of file diff --git a/SharpCAT/SharpCAT.cs b/SharpCAT/SharpCAT.cs deleted file mode 100644 index ccad4b2..0000000 --- a/SharpCAT/SharpCAT.cs +++ /dev/null @@ -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 ports = new List(); - - foreach (string port in portnames) - { - //Testing - ports.Add(new SerialPort(port, (int)BaudRates.ThirtyEightFour, Parity.None, (int)StopBits.Two, (int)Handshake.None)); - } - return ""; - } - } -} \ No newline at end of file diff --git a/SharpCAT/SharpCATLib.csproj b/SharpCAT/SharpCATLib.csproj deleted file mode 100644 index 0779341..0000000 --- a/SharpCAT/SharpCATLib.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - netstandard2.0 - - - - default - - - default - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - MSBuild:Compile - - - MSBuild:Compile - - - - - - - diff --git a/SharpCAT/Socket.cs b/SharpCAT/Socket.cs deleted file mode 100644 index d51f562..0000000 --- a/SharpCAT/Socket.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpCATLib -{ - internal class Socket - { - } -} \ No newline at end of file diff --git a/src/SharpCAT.Client/Program.cs b/src/SharpCAT.Client/Program.cs new file mode 100644 index 0000000..3751555 --- /dev/null +++ b/src/SharpCAT.Client/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/src/SharpCAT.Client/SharpCAT.Client.csproj b/src/SharpCAT.Client/SharpCAT.Client.csproj new file mode 100644 index 0000000..b24b673 --- /dev/null +++ b/src/SharpCAT.Client/SharpCAT.Client.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/SharpCAT.Common/CAT/CATCommand.cs b/src/SharpCAT.Common/CAT/CATCommand.cs new file mode 100644 index 0000000..dd382b4 --- /dev/null +++ b/src/SharpCAT.Common/CAT/CATCommand.cs @@ -0,0 +1,42 @@ +namespace SharpCAT.Common.CAT; + +/// +/// Represents a CAT (Computer Aided Transceiver) command +/// +public class CATCommand +{ + /// + /// Unique identifier for the command + /// + public string Id { get; set; } = string.Empty; + + /// + /// Human-readable name of the command + /// + public string Name { get; set; } = string.Empty; + + /// + /// Description of what the command does + /// + public string Description { get; set; } = string.Empty; + + /// + /// The raw command bytes to send to the radio + /// + public byte[] CommandBytes { get; set; } = Array.Empty(); + + /// + /// Expected response pattern (if any) + /// + public byte[]? ExpectedResponse { get; set; } + + /// + /// Parameters that can be included with the command + /// + public Dictionary Parameters { get; set; } = new(); + + /// + /// Command timeout in milliseconds + /// + public int TimeoutMs { get; set; } = 1000; +} \ No newline at end of file diff --git a/src/SharpCAT.Common/CAT/CATResponse.cs b/src/SharpCAT.Common/CAT/CATResponse.cs new file mode 100644 index 0000000..52000c8 --- /dev/null +++ b/src/SharpCAT.Common/CAT/CATResponse.cs @@ -0,0 +1,42 @@ +namespace SharpCAT.Common.CAT; + +/// +/// Represents the response from a CAT command +/// +public class CATResponse +{ + /// + /// The original command that generated this response + /// + public CATCommand Command { get; set; } = new(); + + /// + /// Whether the command was successful + /// + public bool Success { get; set; } + + /// + /// Raw response bytes from the radio + /// + public byte[] ResponseBytes { get; set; } = Array.Empty(); + + /// + /// Parsed response data (if applicable) + /// + public Dictionary Data { get; set; } = new(); + + /// + /// Error message if the command failed + /// + public string? ErrorMessage { get; set; } + + /// + /// Timestamp when the response was received + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + /// + /// Duration it took to receive the response + /// + public TimeSpan Duration { get; set; } +} \ No newline at end of file diff --git a/src/SharpCAT.Common/CAT/ICATInterface.cs b/src/SharpCAT.Common/CAT/ICATInterface.cs new file mode 100644 index 0000000..99f587b --- /dev/null +++ b/src/SharpCAT.Common/CAT/ICATInterface.cs @@ -0,0 +1,37 @@ +namespace SharpCAT.Common.CAT; + +/// +/// Interface for CAT communication +/// +public interface ICATInterface +{ + /// + /// Sends a CAT command and waits for a response + /// + /// The command to send + /// Cancellation token + /// The response from the radio + Task SendCommandAsync(CATCommand command, CancellationToken cancellationToken = default); + + /// + /// Sends a CAT command without waiting for a response + /// + /// The command to send + /// Cancellation token + Task SendCommandOnlyAsync(CATCommand command, CancellationToken cancellationToken = default); + + /// + /// Checks if the CAT interface is connected and ready + /// + bool IsConnected { get; } + + /// + /// Event fired when a response is received + /// + event EventHandler? ResponseReceived; + + /// + /// Event fired when an error occurs + /// + event EventHandler? ErrorOccurred; +} \ No newline at end of file diff --git a/src/SharpCAT.Common/Certificates/CertificateManager.cs b/src/SharpCAT.Common/Certificates/CertificateManager.cs new file mode 100644 index 0000000..48e5677 --- /dev/null +++ b/src/SharpCAT.Common/Certificates/CertificateManager.cs @@ -0,0 +1,259 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using SharpCAT.Common.Models; + +namespace SharpCAT.Common.Certificates; + +/// +/// Certificate manager for TLS mutual authentication +/// +public class CertificateManager +{ + private readonly CertificateConfig _config; + + public CertificateManager(CertificateConfig config) + { + _config = config; + } + + /// + /// Ensures all required certificates exist, generating them if necessary + /// + 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(); + } + } + + /// + /// Loads the CA certificate + /// + public X509Certificate2 LoadCACertificate() + { + var certPath = GetCACertificatePath(); + if (!File.Exists(certPath)) + throw new FileNotFoundException($"CA certificate not found at {certPath}"); + + return new X509Certificate2(certPath); + } + + /// + /// Loads the server certificate with private key + /// + 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); + } + + /// + /// Loads the client certificate with private key + /// + 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); + } + + /// + /// Validates a certificate against the CA + /// + 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"); +} \ No newline at end of file diff --git a/src/SharpCAT.Common/Configuration/ConfigurationLoader.cs b/src/SharpCAT.Common/Configuration/ConfigurationLoader.cs new file mode 100644 index 0000000..74febfb --- /dev/null +++ b/src/SharpCAT.Common/Configuration/ConfigurationLoader.cs @@ -0,0 +1,173 @@ +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using SharpCAT.Common.Models; + +namespace SharpCAT.Common.Configuration; + +/// +/// Configuration loader for SharpCAT applications +/// +public static class ConfigurationLoader +{ + /// + /// Loads server configuration from JSON file + /// + /// Path to the configuration file + /// Server configuration + public static ServerConfig LoadServerConfig(string configPath = "server-config.json") + { + return LoadConfig(configPath, GetDefaultServerConfig()); + } + + /// + /// Loads client configuration from JSON file + /// + /// Path to the configuration file + /// Client configuration + public static ClientConfig LoadClientConfig(string configPath = "client-config.json") + { + return LoadConfig(configPath, GetDefaultClientConfig()); + } + + /// + /// Saves server configuration to JSON file + /// + /// Configuration to save + /// Path to save the configuration file + public static async Task SaveServerConfigAsync(ServerConfig config, string configPath = "server-config.json") + { + await SaveConfigAsync(config, configPath); + } + + /// + /// Saves client configuration to JSON file + /// + /// Configuration to save + /// Path to save the configuration file + public static async Task SaveClientConfigAsync(ClientConfig config, string configPath = "client-config.json") + { + await SaveConfigAsync(config, configPath); + } + + /// + /// Creates default server configuration file if it doesn't exist + /// + /// Path to create the configuration file + public static async Task EnsureServerConfigExistsAsync(string configPath = "server-config.json") + { + if (!File.Exists(configPath)) + { + await SaveServerConfigAsync(GetDefaultServerConfig(), configPath); + } + } + + /// + /// Creates default client configuration file if it doesn't exist + /// + /// Path to create the configuration file + public static async Task EnsureClientConfigExistsAsync(string configPath = "client-config.json") + { + if (!File.Exists(configPath)) + { + await SaveClientConfigAsync(GetDefaultClientConfig(), configPath); + } + } + + private static T LoadConfig(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(); + return config ?? defaultConfig; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to load configuration from {configPath}: {ex.Message}", ex); + } + } + + private static async Task SaveConfigAsync(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 + }; + } +} \ No newline at end of file diff --git a/src/SharpCAT.Common/Logging/SharpCATLogger.cs b/src/SharpCAT.Common/Logging/SharpCATLogger.cs new file mode 100644 index 0000000..05247c9 --- /dev/null +++ b/src/SharpCAT.Common/Logging/SharpCATLogger.cs @@ -0,0 +1,230 @@ +using Microsoft.Extensions.Logging; +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace SharpCAT.Common.Logging; + +/// +/// Custom logger provider that supports both console and native OS logging +/// +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 + } +} + +/// +/// Custom logger that supports both console and native OS logging +/// +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 state) => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel != LogLevel.None; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func 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() { } + } +} \ No newline at end of file diff --git a/src/SharpCAT.Common/Models/CertificateConfig.cs b/src/SharpCAT.Common/Models/CertificateConfig.cs new file mode 100644 index 0000000..c30e665 --- /dev/null +++ b/src/SharpCAT.Common/Models/CertificateConfig.cs @@ -0,0 +1,52 @@ +namespace SharpCAT.Common.Models; + +/// +/// Certificate configuration for TLS mutual authentication +/// +public class CertificateConfig +{ + /// + /// Path to the Certificate Authority (CA) certificate file + /// + public string? CaCertificatePath { get; set; } + + /// + /// Path to the server certificate file + /// + public string? ServerCertificatePath { get; set; } + + /// + /// Path to the server private key file + /// + public string? ServerKeyPath { get; set; } + + /// + /// Path to the client certificate file + /// + public string? ClientCertificatePath { get; set; } + + /// + /// Path to the client private key file + /// + public string? ClientKeyPath { get; set; } + + /// + /// Certificate store location for storing generated certificates + /// + public string CertificateStorePath { get; set; } = "./certificates"; + + /// + /// Whether to auto-generate certificates if they don't exist + /// + public bool AutoGenerateCertificates { get; set; } = true; + + /// + /// Certificate validity period in days + /// + public int ValidityDays { get; set; } = 365; + + /// + /// Subject name for generated certificates + /// + public string Subject { get; set; } = "CN=SharpCAT"; +} \ No newline at end of file diff --git a/src/SharpCAT.Common/Models/ClientConfig.cs b/src/SharpCAT.Common/Models/ClientConfig.cs new file mode 100644 index 0000000..a27e814 --- /dev/null +++ b/src/SharpCAT.Common/Models/ClientConfig.cs @@ -0,0 +1,32 @@ +namespace SharpCAT.Common.Models; + +/// +/// Configuration model for the SharpCAT client +/// +public class ClientConfig +{ + /// + /// Server hostname or IP address to connect to + /// + public string ServerHost { get; set; } = "localhost"; + + /// + /// Server port to connect to + /// + public int ServerPort { get; set; } = 8443; + + /// + /// Certificate configuration for TLS + /// + public CertificateConfig Certificate { get; set; } = new(); + + /// + /// Logging configuration + /// + public LoggingConfig Logging { get; set; } = new(); + + /// + /// Connection timeout in milliseconds + /// + public int ConnectionTimeoutMs { get; set; } = 5000; +} \ No newline at end of file diff --git a/src/SharpCAT.Common/Models/LoggingConfig.cs b/src/SharpCAT.Common/Models/LoggingConfig.cs new file mode 100644 index 0000000..f3af937 --- /dev/null +++ b/src/SharpCAT.Common/Models/LoggingConfig.cs @@ -0,0 +1,37 @@ +namespace SharpCAT.Common.Models; + +/// +/// Logging configuration +/// +public class LoggingConfig +{ + /// + /// Minimum log level + /// + public string LogLevel { get; set; } = "Information"; + + /// + /// Whether to enable console logging + /// + public bool EnableConsoleLogging { get; set; } = true; + + /// + /// Whether to enable native OS logging (Event Log on Windows, syslog on Linux/macOS) + /// + public bool EnableOSLogging { get; set; } = true; + + /// + /// Log file path (optional) + /// + public string? LogFilePath { get; set; } + + /// + /// Whether to include timestamps in console output + /// + public bool IncludeTimestamps { get; set; } = true; + + /// + /// Whether to include log levels in console output + /// + public bool IncludeLogLevel { get; set; } = true; +} \ No newline at end of file diff --git a/src/SharpCAT.Common/Models/SerialPortConfig.cs b/src/SharpCAT.Common/Models/SerialPortConfig.cs new file mode 100644 index 0000000..554f041 --- /dev/null +++ b/src/SharpCAT.Common/Models/SerialPortConfig.cs @@ -0,0 +1,52 @@ +namespace SharpCAT.Common.Models; + +/// +/// Serial port configuration for CAT communication +/// +public class SerialPortConfig +{ + /// + /// Serial port name (e.g., COM1, /dev/ttyUSB0) + /// + public string? PortName { get; set; } + + /// + /// Baud rate for serial communication + /// + public int BaudRate { get; set; } = 9600; + + /// + /// Data bits + /// + public int DataBits { get; set; } = 8; + + /// + /// Stop bits + /// + public System.IO.Ports.StopBits StopBits { get; set; } = System.IO.Ports.StopBits.One; + + /// + /// Parity setting + /// + public System.IO.Ports.Parity Parity { get; set; } = System.IO.Ports.Parity.None; + + /// + /// Flow control/handshake + /// + public System.IO.Ports.Handshake Handshake { get; set; } = System.IO.Ports.Handshake.None; + + /// + /// Read timeout in milliseconds + /// + public int ReadTimeoutMs { get; set; } = 1000; + + /// + /// Write timeout in milliseconds + /// + public int WriteTimeoutMs { get; set; } = 1000; + + /// + /// Whether to auto-detect the serial port if not specified + /// + public bool AutoDetectPort { get; set; } = true; +} \ No newline at end of file diff --git a/src/SharpCAT.Common/Models/ServerConfig.cs b/src/SharpCAT.Common/Models/ServerConfig.cs new file mode 100644 index 0000000..bbe296d --- /dev/null +++ b/src/SharpCAT.Common/Models/ServerConfig.cs @@ -0,0 +1,32 @@ +namespace SharpCAT.Common.Models; + +/// +/// Configuration model for the SharpCAT server +/// +public class ServerConfig +{ + /// + /// TCP port to listen on + /// + public int Port { get; set; } = 8443; + + /// + /// Certificate configuration for TLS + /// + public CertificateConfig Certificate { get; set; } = new(); + + /// + /// Serial port configuration for CAT communication + /// + public SerialPortConfig SerialPort { get; set; } = new(); + + /// + /// Logging configuration + /// + public LoggingConfig Logging { get; set; } = new(); + + /// + /// Maximum number of concurrent clients + /// + public int MaxClients { get; set; } = 10; +} \ No newline at end of file diff --git a/src/SharpCAT.Common/SerialPort/SerialPortHelper.cs b/src/SharpCAT.Common/SerialPort/SerialPortHelper.cs new file mode 100644 index 0000000..de81de0 --- /dev/null +++ b/src/SharpCAT.Common/SerialPort/SerialPortHelper.cs @@ -0,0 +1,173 @@ +using System.IO.Ports; +using System.Runtime.InteropServices; + +namespace SharpCAT.Common.SerialPort; + +/// +/// Cross-platform serial port helper utilities +/// +public static class SerialPortHelper +{ + /// + /// Gets available serial port names for the current platform + /// + /// Array of available serial port names + 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(); + } + } + + /// + /// Auto-detects the best serial port for CAT communication + /// + /// The detected port name, or null if none found + public static async Task 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(); + 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(); + + // 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(); + + 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(); + } +} \ No newline at end of file diff --git a/src/SharpCAT.Common/SharpCAT.Common.csproj b/src/SharpCAT.Common/SharpCAT.Common.csproj new file mode 100644 index 0000000..dedd89d --- /dev/null +++ b/src/SharpCAT.Common/SharpCAT.Common.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/src/SharpCAT.Server/Program.cs b/src/SharpCAT.Server/Program.cs new file mode 100644 index 0000000..3751555 --- /dev/null +++ b/src/SharpCAT.Server/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/src/SharpCAT.Server/SharpCAT.Server.csproj b/src/SharpCAT.Server/SharpCAT.Server.csproj new file mode 100644 index 0000000..b24b673 --- /dev/null +++ b/src/SharpCAT.Server/SharpCAT.Server.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + + +