diff --git a/README.md b/README.md
index 6de175e..f4e17f9 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,31 @@
# This like many, are ideas. There may or may not be progress as inspiration and time allows.
# SharpCAT
-C#, .NET Standard based CAT control library.
+C#, .NET Standard based CAT control library with ASP.NET Core Web API server.
I am targeting .Net Standard so that the assembly may be used with .Net Core or the .Net framework.
+## Components
+
+### SharpCAT Library
+The core .NET Standard 2.0 library for CAT (Computer Aided Transceiver) control.
+
+### SharpCAT Server
+A cross-platform ASP.NET Core Web API server that provides REST endpoints for CAT control operations. Located in `Server/SharpCAT.Server/`.
+
+**Key Features:**
+- REST API for serial port management and CAT commands
+- Cross-platform support (Windows, Linux, macOS)
+- Swagger/OpenAPI documentation
+- Built-in error handling and logging
+
+**Quick Start:**
+```bash
+cd Server/SharpCAT.Server
+dotnet run
+```
+Then visit `http://localhost:5188` for the Swagger UI.
+
## Development Setup
### Prerequisites
@@ -16,19 +37,30 @@ I am targeting .Net Standard so that the assembly may be used with .Net Core or
This project is configured for development in Visual Studio Code with the .NET CLI.
-**To build the project:**
+**To build the entire solution:**
```bash
-dotnet build SharpCAT/SharpCATLib.csproj
+dotnet build
+```
+
+**To build just the library:**
+```bash
+dotnet build SharpCAT/SharpCAT.csproj
+```
+
+**To build and run the Web API server:**
+```bash
+cd Server/SharpCAT.Server
+dotnet run
```
**To clean the project:**
```bash
-dotnet clean SharpCAT/SharpCATLib.csproj
+dotnet clean
```
**To restore packages:**
```bash
-dotnet restore SharpCAT/SharpCATLib.csproj
+dotnet restore
```
**VS Code Tasks:**
diff --git a/Server/SharpCAT.Server/Controllers/CatController.cs b/Server/SharpCAT.Server/Controllers/CatController.cs
new file mode 100644
index 0000000..7907353
--- /dev/null
+++ b/Server/SharpCAT.Server/Controllers/CatController.cs
@@ -0,0 +1,292 @@
+using Microsoft.AspNetCore.Mvc;
+using SharpCAT.Server.Models;
+using SharpCAT.Server.Services;
+
+namespace SharpCAT.Server.Controllers
+{
+ ///
+ /// Controller for CAT (Computer Aided Transceiver) operations
+ ///
+ [ApiController]
+ [Route("api/[controller]")]
+ [Produces("application/json")]
+ public class CatController : ControllerBase
+ {
+ private readonly ISerialCommunicationService _serialService;
+ private readonly ILogger _logger;
+
+ public CatController(ISerialCommunicationService serialService, ILogger logger)
+ {
+ _serialService = serialService ?? throw new ArgumentNullException(nameof(serialService));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// Gets all available serial ports on the system
+ ///
+ /// List of available serial ports
+ /// Returns the list of available serial ports
+ [HttpGet("ports")]
+ [ProducesResponseType(typeof(PortListResponse), StatusCodes.Status200OK)]
+ public ActionResult GetPorts()
+ {
+ try
+ {
+ _logger.LogInformation("Getting available serial ports");
+ var ports = _serialService.GetAvailablePorts();
+
+ var response = new PortListResponse
+ {
+ Ports = ports
+ };
+
+ _logger.LogInformation("Found {PortCount} available ports", response.Count);
+ return Ok(response);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting available ports");
+ return StatusCode(500, new ErrorResponse
+ {
+ Error = "Internal server error while getting ports",
+ Details = ex.Message
+ });
+ }
+ }
+
+ ///
+ /// Opens and configures a serial port for communication
+ ///
+ /// Port configuration parameters
+ /// Result of the port open operation
+ /// Port opened successfully
+ /// Invalid request parameters
+ /// Internal server error
+ [HttpPost("open")]
+ [ProducesResponseType(typeof(PortOperationResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
+ public async Task> OpenPort([FromBody] OpenPortRequest request)
+ {
+ try
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ErrorResponse
+ {
+ Error = "Invalid request parameters",
+ Details = string.Join("; ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage))
+ });
+ }
+
+ _logger.LogInformation("Attempting to open port {PortName} with baud rate {BaudRate}",
+ request.PortName, request.BaudRate);
+
+ var success = await _serialService.OpenPortAsync(
+ request.PortName,
+ request.BaudRate,
+ request.Parity,
+ request.StopBits,
+ request.Handshake);
+
+ var response = new PortOperationResponse
+ {
+ Success = success,
+ PortName = request.PortName,
+ IsOpen = _serialService.IsPortOpen(),
+ Message = success
+ ? $"Port {request.PortName} opened successfully"
+ : $"Failed to open port {request.PortName}"
+ };
+
+ if (success)
+ {
+ _logger.LogInformation("Successfully opened port {PortName}", request.PortName);
+ return Ok(response);
+ }
+ else
+ {
+ _logger.LogWarning("Failed to open port {PortName}", request.PortName);
+ return StatusCode(500, new ErrorResponse
+ {
+ Error = response.Message
+ });
+ }
+ }
+ catch (ArgumentException ex)
+ {
+ _logger.LogWarning(ex, "Invalid argument for opening port: {PortName}", request.PortName);
+ return BadRequest(new ErrorResponse
+ {
+ Error = "Invalid port parameters",
+ Details = ex.Message
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error opening port: {PortName}", request.PortName);
+ return StatusCode(500, new ErrorResponse
+ {
+ Error = "Internal server error while opening port",
+ Details = ex.Message
+ });
+ }
+ }
+
+ ///
+ /// Closes the currently opened serial port
+ ///
+ /// Result of the port close operation
+ /// Port closed successfully
+ /// Internal server error
+ [HttpPost("close")]
+ [ProducesResponseType(typeof(PortOperationResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
+ public async Task> ClosePort()
+ {
+ try
+ {
+ var currentPortName = _serialService.GetCurrentPortName();
+ _logger.LogInformation("Attempting to close port {PortName}", currentPortName ?? "unknown");
+
+ await _serialService.ClosePortAsync();
+
+ var response = new PortOperationResponse
+ {
+ Success = true,
+ PortName = currentPortName,
+ IsOpen = _serialService.IsPortOpen(),
+ Message = "Port closed successfully"
+ };
+
+ _logger.LogInformation("Successfully closed port");
+ return Ok(response);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error closing port");
+ return StatusCode(500, new ErrorResponse
+ {
+ Error = "Internal server error while closing port",
+ Details = ex.Message
+ });
+ }
+ }
+
+ ///
+ /// Sends a CAT command to the connected radio
+ ///
+ /// CAT command to send
+ /// Result of the command operation including any response from the radio
+ /// Command sent successfully
+ /// Invalid request parameters or no port open
+ /// Internal server error
+ [HttpPost("command")]
+ [ProducesResponseType(typeof(CommandResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
+ public async Task> SendCommand([FromBody] SendCommandRequest request)
+ {
+ try
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ErrorResponse
+ {
+ Error = "Invalid request parameters",
+ Details = string.Join("; ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage))
+ });
+ }
+
+ if (!_serialService.IsPortOpen())
+ {
+ return BadRequest(new ErrorResponse
+ {
+ Error = "No serial port is currently open",
+ Details = "You must open a serial port before sending commands"
+ });
+ }
+
+ _logger.LogInformation("Sending CAT command: {Command}", request.Command);
+
+ var radioResponse = await _serialService.SendCommandAsync(request.Command);
+
+ var response = new CommandResponse
+ {
+ Success = true,
+ Command = request.Command,
+ Response = radioResponse,
+ Message = "Command sent successfully",
+ Timestamp = DateTime.UtcNow
+ };
+
+ _logger.LogInformation("CAT command sent successfully. Response: {Response}", radioResponse);
+ return Ok(response);
+ }
+ catch (ArgumentException ex)
+ {
+ _logger.LogWarning(ex, "Invalid command: {Command}", request.Command);
+ return BadRequest(new ErrorResponse
+ {
+ Error = "Invalid command",
+ Details = ex.Message
+ });
+ }
+ catch (InvalidOperationException ex)
+ {
+ _logger.LogWarning(ex, "Cannot send command - port not open");
+ return BadRequest(new ErrorResponse
+ {
+ Error = "Cannot send command",
+ Details = ex.Message
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error sending CAT command: {Command}", request.Command);
+ return StatusCode(500, new ErrorResponse
+ {
+ Error = "Internal server error while sending command",
+ Details = ex.Message
+ });
+ }
+ }
+
+ ///
+ /// Gets the current status of the serial port connection
+ ///
+ /// Current port status
+ /// Returns current port status
+ [HttpGet("status")]
+ [ProducesResponseType(typeof(PortOperationResponse), StatusCodes.Status200OK)]
+ public ActionResult GetStatus()
+ {
+ try
+ {
+ var isOpen = _serialService.IsPortOpen();
+ var currentPort = _serialService.GetCurrentPortName();
+
+ var response = new PortOperationResponse
+ {
+ Success = true,
+ PortName = currentPort,
+ IsOpen = isOpen,
+ Message = isOpen
+ ? $"Port {currentPort} is open"
+ : "No port is currently open"
+ };
+
+ return Ok(response);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting port status");
+ return StatusCode(500, new ErrorResponse
+ {
+ Error = "Internal server error while getting status",
+ Details = ex.Message
+ });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Server/SharpCAT.Server/Models/ApiModels.cs b/Server/SharpCAT.Server/Models/ApiModels.cs
new file mode 100644
index 0000000..06b7109
--- /dev/null
+++ b/Server/SharpCAT.Server/Models/ApiModels.cs
@@ -0,0 +1,143 @@
+using System.ComponentModel.DataAnnotations;
+using System.IO.Ports;
+
+namespace SharpCAT.Server.Models
+{
+ ///
+ /// Response model for listing available serial ports
+ ///
+ public class PortListResponse
+ {
+ ///
+ /// Array of available serial port names
+ ///
+ public string[] Ports { get; set; } = Array.Empty();
+
+ ///
+ /// Number of available ports
+ ///
+ public int Count => Ports.Length;
+ }
+
+ ///
+ /// Request model for opening a serial port
+ ///
+ public class OpenPortRequest
+ {
+ ///
+ /// Name of the serial port to open (e.g., "COM1", "/dev/ttyUSB0")
+ ///
+ [Required]
+ public string PortName { get; set; } = string.Empty;
+
+ ///
+ /// Baud rate for communication (default: 9600)
+ ///
+ public int BaudRate { get; set; } = 9600;
+
+ ///
+ /// Parity setting (default: None)
+ ///
+ public Parity Parity { get; set; } = Parity.None;
+
+ ///
+ /// Stop bits setting (default: One)
+ ///
+ public StopBits StopBits { get; set; } = StopBits.One;
+
+ ///
+ /// Handshake setting (default: None)
+ ///
+ public Handshake Handshake { get; set; } = Handshake.None;
+ }
+
+ ///
+ /// Response model for port operations
+ ///
+ public class PortOperationResponse
+ {
+ ///
+ /// Indicates if the operation was successful
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Human-readable message about the operation
+ ///
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// Name of the port that was operated on
+ ///
+ public string? PortName { get; set; }
+
+ ///
+ /// Current status of the port (open/closed)
+ ///
+ public bool IsOpen { get; set; }
+ }
+
+ ///
+ /// Request model for sending CAT commands
+ ///
+ public class SendCommandRequest
+ {
+ ///
+ /// CAT command string to send to the radio
+ ///
+ [Required]
+ public string Command { get; set; } = string.Empty;
+ }
+
+ ///
+ /// Response model for CAT command operations
+ ///
+ public class CommandResponse
+ {
+ ///
+ /// Indicates if the command was sent successfully
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// The command that was sent
+ ///
+ public string Command { get; set; } = string.Empty;
+
+ ///
+ /// Response received from the radio (if any)
+ ///
+ public string? Response { get; set; }
+
+ ///
+ /// Human-readable message about the operation
+ ///
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// Timestamp when the command was executed
+ ///
+ public DateTime Timestamp { get; set; } = DateTime.UtcNow;
+ }
+
+ ///
+ /// Response model for errors
+ ///
+ public class ErrorResponse
+ {
+ ///
+ /// Error message
+ ///
+ public string Error { get; set; } = string.Empty;
+
+ ///
+ /// Additional details about the error
+ ///
+ public string? Details { get; set; }
+
+ ///
+ /// Timestamp when the error occurred
+ ///
+ public DateTime Timestamp { get; set; } = DateTime.UtcNow;
+ }
+}
\ No newline at end of file
diff --git a/Server/SharpCAT.Server/Program.cs b/Server/SharpCAT.Server/Program.cs
new file mode 100644
index 0000000..25070fc
--- /dev/null
+++ b/Server/SharpCAT.Server/Program.cs
@@ -0,0 +1,67 @@
+using SharpCAT.Server.Services;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
+ {
+ Title = "SharpCAT Server API",
+ Version = "v1",
+ Description = "ASP.NET Core Web API for CAT (Computer Aided Transceiver) control using SharpCAT library",
+ Contact = new Microsoft.OpenApi.Models.OpenApiContact
+ {
+ Name = "SharpCAT Project"
+ }
+ });
+
+ // Include XML comments if available
+ var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename);
+ if (File.Exists(xmlPath))
+ {
+ c.IncludeXmlComments(xmlPath);
+ }
+});
+
+// Register the serial communication service as a singleton
+// to maintain connection state across requests
+builder.Services.AddSingleton();
+
+// Add CORS policy for cross-origin requests
+builder.Services.AddCors(options =>
+{
+ options.AddPolicy("AllowAll", policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+});
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI(c =>
+ {
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "SharpCAT Server API v1");
+ c.RoutePrefix = string.Empty; // Set Swagger UI at app's root
+ });
+}
+
+app.UseCors("AllowAll");
+app.UseRouting();
+app.MapControllers();
+
+// Add a simple health check endpoint
+app.MapGet("/health", () => new { status = "healthy", timestamp = DateTime.UtcNow })
+ .WithName("HealthCheck")
+ .WithOpenApi();
+
+app.Run();
diff --git a/Server/SharpCAT.Server/Properties/launchSettings.json b/Server/SharpCAT.Server/Properties/launchSettings.json
new file mode 100644
index 0000000..cc2744d
--- /dev/null
+++ b/Server/SharpCAT.Server/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:14425",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5188",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Server/SharpCAT.Server/README.md b/Server/SharpCAT.Server/README.md
new file mode 100644
index 0000000..4d9218a
--- /dev/null
+++ b/Server/SharpCAT.Server/README.md
@@ -0,0 +1,150 @@
+# SharpCAT Server
+
+A cross-platform ASP.NET Core Web API server that provides REST endpoints for CAT (Computer Aided Transceiver) control using the SharpCAT library.
+
+## Features
+
+- **Cross-platform**: Works on Windows, Linux, and macOS
+- **REST API**: Clean, documented REST endpoints for radio control
+- **Serial Communication**: Leverages the SharpCAT core library for serial port operations
+- **Swagger Documentation**: Built-in API documentation and testing interface
+- **Error Handling**: Comprehensive error handling and logging
+
+## API Endpoints
+
+### Serial Port Management
+
+- `GET /api/cat/ports` - List all available serial ports
+- `POST /api/cat/open` - Open and configure a serial port
+- `POST /api/cat/close` - Close the currently opened port
+- `GET /api/cat/status` - Get current port connection status
+
+### CAT Commands
+
+- `POST /api/cat/command` - Send arbitrary CAT commands to the radio
+
+### Health Check
+
+- `GET /health` - Simple health check endpoint
+
+## Quick Start
+
+### Prerequisites
+
+- [.NET 8.0 SDK](https://dotnet.microsoft.com/download) or later
+
+### Running the Server
+
+1. Navigate to the server directory:
+ ```bash
+ cd Server/SharpCAT.Server
+ ```
+
+2. Build and run the server:
+ ```bash
+ dotnet run
+ ```
+
+3. The server will start and listen on `http://localhost:5188` by default
+
+4. Open your browser to `http://localhost:5188` to access the Swagger UI for interactive API documentation
+
+### Example Usage
+
+#### List Available Ports
+```bash
+curl http://localhost:5188/api/cat/ports
+```
+
+#### Open a Serial Port
+```bash
+curl -X POST http://localhost:5188/api/cat/open \
+ -H "Content-Type: application/json" \
+ -d '{
+ "portName": "COM3",
+ "baudRate": 9600,
+ "parity": "None",
+ "stopBits": "One",
+ "handshake": "None"
+ }'
+```
+
+#### Send a CAT Command
+```bash
+curl -X POST http://localhost:5188/api/cat/command \
+ -H "Content-Type: application/json" \
+ -d '{"command": "FA;"}'
+```
+
+#### Check Status
+```bash
+curl http://localhost:5188/api/cat/status
+```
+
+## Configuration
+
+### Port Configuration Options
+
+When opening a port, you can configure:
+
+- **Port Name**: Serial port name (e.g., "COM1" on Windows, "/dev/ttyUSB0" on Linux)
+- **Baud Rate**: Communication speed (default: 9600)
+- **Parity**: Error checking method (None, Odd, Even, Mark, Space)
+- **Stop Bits**: Number of stop bits (None, One, Two, OnePointFive)
+- **Handshake**: Flow control method (None, XOnXOff, RequestToSend, RequestToSendXOnXOff)
+
+### Logging
+
+The server uses ASP.NET Core's built-in logging. Log levels can be configured in `appsettings.json`:
+
+```json
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "SharpCAT.Server": "Debug"
+ }
+ }
+}
+```
+
+## Development
+
+### Building from Source
+
+1. Ensure you have the .NET 8.0 SDK installed
+2. Clone the repository
+3. Navigate to the project root
+4. Build the solution:
+ ```bash
+ dotnet build
+ ```
+
+### Project Structure
+
+```
+Server/SharpCAT.Server/
+├── Controllers/
+│ └── CatController.cs # Main API controller
+├── Models/
+│ └── ApiModels.cs # Request/response models
+├── Services/
+│ ├── ISerialCommunicationService.cs # Service interface
+│ └── SerialCommunicationService.cs # Service implementation
+├── Program.cs # Application entry point
+└── SharpCAT.Server.csproj # Project file
+```
+
+### Dependencies
+
+- **Microsoft.AspNetCore.OpenApi** - OpenAPI support
+- **Swashbuckle.AspNetCore** - Swagger documentation
+- **SharpCAT** - Core CAT control library
+
+## License
+
+This project follows the same license as the parent SharpCAT project.
+
+## Contributing
+
+Contributions are welcome! Please follow the existing code style and add appropriate tests for new features.
\ No newline at end of file
diff --git a/Server/SharpCAT.Server/Services/ISerialCommunicationService.cs b/Server/SharpCAT.Server/Services/ISerialCommunicationService.cs
new file mode 100644
index 0000000..e8b4c17
--- /dev/null
+++ b/Server/SharpCAT.Server/Services/ISerialCommunicationService.cs
@@ -0,0 +1,51 @@
+using System.IO.Ports;
+
+namespace SharpCAT.Server.Services
+{
+ ///
+ /// Interface for serial communication services
+ ///
+ public interface ISerialCommunicationService
+ {
+ ///
+ /// Gets all available serial ports on the system
+ ///
+ /// Array of port names
+ string[] GetAvailablePorts();
+
+ ///
+ /// Opens and configures a serial port
+ ///
+ /// Name of the port to open
+ /// Baud rate for communication
+ /// Parity setting
+ /// Stop bits setting
+ /// Handshake setting
+ /// True if port opened successfully
+ Task OpenPortAsync(string portName, int baudRate, Parity parity, StopBits stopBits, Handshake handshake);
+
+ ///
+ /// Closes the currently opened port
+ ///
+ Task ClosePortAsync();
+
+ ///
+ /// Sends a CAT command to the radio
+ ///
+ /// CAT command string to send
+ /// Response from the radio, if any
+ Task SendCommandAsync(string command);
+
+ ///
+ /// Gets the status of the current port connection
+ ///
+ /// True if port is open and connected
+ bool IsPortOpen();
+
+ ///
+ /// Gets the name of the currently opened port
+ ///
+ /// Port name or null if no port is open
+ string? GetCurrentPortName();
+ }
+}
\ No newline at end of file
diff --git a/Server/SharpCAT.Server/Services/SerialCommunicationService.cs b/Server/SharpCAT.Server/Services/SerialCommunicationService.cs
new file mode 100644
index 0000000..96193fb
--- /dev/null
+++ b/Server/SharpCAT.Server/Services/SerialCommunicationService.cs
@@ -0,0 +1,208 @@
+using System.IO.Ports;
+using System.Text;
+
+namespace SharpCAT.Server.Services
+{
+ ///
+ /// Service for managing serial communication with radios using SharpCAT library
+ ///
+ public class SerialCommunicationService : ISerialCommunicationService, IDisposable
+ {
+ private readonly ILogger _logger;
+ private Serial? _serialConnection;
+ private SerialPort? _directSerialPort;
+ private string? _currentPortName;
+ private readonly SemaphoreSlim _semaphore = new(1, 1);
+ private bool _disposed = false;
+
+ public SerialCommunicationService(ILogger logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ public string[] GetAvailablePorts()
+ {
+ try
+ {
+ var sharpCat = new SharpCAT();
+ var ports = sharpCat.PortNames;
+ _logger.LogInformation("Found {PortCount} available serial ports", ports?.Length ?? 0);
+ return ports ?? Array.Empty();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting available serial ports");
+ return Array.Empty();
+ }
+ }
+
+ ///
+ public async Task OpenPortAsync(string portName, int baudRate, Parity parity, StopBits stopBits, Handshake handshake)
+ {
+ if (string.IsNullOrWhiteSpace(portName))
+ throw new ArgumentException("Port name cannot be null or empty", nameof(portName));
+
+ await _semaphore.WaitAsync();
+ try
+ {
+ // Close existing connection if any
+ await ClosePortInternalAsync();
+
+ _logger.LogInformation("Attempting to open port {PortName} with baud rate {BaudRate}", portName, baudRate);
+
+ // Create direct SerialPort for more control over communication
+ _directSerialPort = new SerialPort
+ {
+ PortName = portName,
+ BaudRate = baudRate,
+ Parity = parity,
+ StopBits = stopBits,
+ Handshake = handshake,
+ DataBits = 8,
+ ReadTimeout = 5000,
+ WriteTimeout = 5000,
+ Encoding = Encoding.ASCII
+ };
+
+ _directSerialPort.Open();
+ _currentPortName = portName;
+
+ _logger.LogInformation("Successfully opened port {PortName}", portName);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to open port {PortName}", portName);
+ _directSerialPort?.Dispose();
+ _directSerialPort = null;
+ _currentPortName = null;
+ return false;
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ ///
+ public async Task ClosePortAsync()
+ {
+ await _semaphore.WaitAsync();
+ try
+ {
+ await ClosePortInternalAsync();
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ ///
+ public async Task SendCommandAsync(string command)
+ {
+ if (string.IsNullOrWhiteSpace(command))
+ throw new ArgumentException("Command cannot be null or empty", nameof(command));
+
+ await _semaphore.WaitAsync();
+ try
+ {
+ if (_directSerialPort == null || !_directSerialPort.IsOpen)
+ {
+ throw new InvalidOperationException("No serial port is currently open");
+ }
+
+ _logger.LogDebug("Sending command: {Command}", command);
+
+ // Send the command
+ await Task.Run(() => _directSerialPort.WriteLine(command));
+
+ // Wait a bit for response
+ await Task.Delay(100);
+
+ // Try to read response
+ string response = "";
+ if (_directSerialPort.BytesToRead > 0)
+ {
+ response = await Task.Run(() =>
+ {
+ try
+ {
+ return _directSerialPort.ReadExisting();
+ }
+ catch (TimeoutException)
+ {
+ return "";
+ }
+ });
+ }
+
+ _logger.LogDebug("Command response: {Response}", response);
+ return response.Trim();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error sending command: {Command}", command);
+ throw;
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ ///
+ public bool IsPortOpen()
+ {
+ return _directSerialPort?.IsOpen == true;
+ }
+
+ ///
+ public string? GetCurrentPortName()
+ {
+ return _currentPortName;
+ }
+
+ private async Task ClosePortInternalAsync()
+ {
+ if (_directSerialPort != null)
+ {
+ try
+ {
+ if (_directSerialPort.IsOpen)
+ {
+ await Task.Run(() => _directSerialPort.Close());
+ }
+ _directSerialPort.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error closing serial port");
+ }
+ finally
+ {
+ _directSerialPort = null;
+ _currentPortName = null;
+ }
+ }
+
+ if (_serialConnection != null)
+ {
+ _serialConnection = null;
+ }
+
+ _logger.LogInformation("Serial port closed");
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ ClosePortInternalAsync().Wait();
+ _semaphore?.Dispose();
+ _disposed = true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Server/SharpCAT.Server/SharpCAT.Server.csproj b/Server/SharpCAT.Server/SharpCAT.Server.csproj
new file mode 100644
index 0000000..a865745
--- /dev/null
+++ b/Server/SharpCAT.Server/SharpCAT.Server.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Server/SharpCAT.Server/SharpCAT.Server.http b/Server/SharpCAT.Server/SharpCAT.Server.http
new file mode 100644
index 0000000..0f8cad5
--- /dev/null
+++ b/Server/SharpCAT.Server/SharpCAT.Server.http
@@ -0,0 +1,6 @@
+@SharpCAT.Server_HostAddress = http://localhost:5188
+
+GET {{SharpCAT.Server_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/Server/SharpCAT.Server/appsettings.Development.json b/Server/SharpCAT.Server/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/Server/SharpCAT.Server/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Server/SharpCAT.Server/appsettings.json b/Server/SharpCAT.Server/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/Server/SharpCAT.Server/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/SharpCAT.sln b/SharpCAT.sln
new file mode 100644
index 0000000..a8a1593
--- /dev/null
+++ b/SharpCAT.sln
@@ -0,0 +1,33 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCAT", "SharpCAT\SharpCAT.csproj", "{3EA807EF-B181-4C54-8502-0A2A3EACE984}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{43CFF66C-84E6-4EC2-AE4F-005FB80D74D5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCAT.Server", "Server\SharpCAT.Server\SharpCAT.Server.csproj", "{22E4F655-252E-42DB-ADD5-494BC825A97C}"
+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
+ {3EA807EF-B181-4C54-8502-0A2A3EACE984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3EA807EF-B181-4C54-8502-0A2A3EACE984}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3EA807EF-B181-4C54-8502-0A2A3EACE984}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3EA807EF-B181-4C54-8502-0A2A3EACE984}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22E4F655-252E-42DB-ADD5-494BC825A97C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22E4F655-252E-42DB-ADD5-494BC825A97C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22E4F655-252E-42DB-ADD5-494BC825A97C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22E4F655-252E-42DB-ADD5-494BC825A97C}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {22E4F655-252E-42DB-ADD5-494BC825A97C} = {43CFF66C-84E6-4EC2-AE4F-005FB80D74D5}
+ EndGlobalSection
+EndGlobal