mirror of
https://github.com/ekinnee/SharpCAT.git
synced 2025-12-06 03:31:59 +01:00
Implement ASP.NET Core Web API server for SharpCAT with REST endpoints
Co-authored-by: ekinnee <1707617+ekinnee@users.noreply.github.com>
This commit is contained in:
parent
ce86879a3b
commit
c20d3a12bb
42
README.md
42
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:**
|
||||
|
|
|
|||
292
Server/SharpCAT.Server/Controllers/CatController.cs
Normal file
292
Server/SharpCAT.Server/Controllers/CatController.cs
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharpCAT.Server.Models;
|
||||
using SharpCAT.Server.Services;
|
||||
|
||||
namespace SharpCAT.Server.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for CAT (Computer Aided Transceiver) operations
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Produces("application/json")]
|
||||
public class CatController : ControllerBase
|
||||
{
|
||||
private readonly ISerialCommunicationService _serialService;
|
||||
private readonly ILogger<CatController> _logger;
|
||||
|
||||
public CatController(ISerialCommunicationService serialService, ILogger<CatController> logger)
|
||||
{
|
||||
_serialService = serialService ?? throw new ArgumentNullException(nameof(serialService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all available serial ports on the system
|
||||
/// </summary>
|
||||
/// <returns>List of available serial ports</returns>
|
||||
/// <response code="200">Returns the list of available serial ports</response>
|
||||
[HttpGet("ports")]
|
||||
[ProducesResponseType(typeof(PortListResponse), StatusCodes.Status200OK)]
|
||||
public ActionResult<PortListResponse> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens and configures a serial port for communication
|
||||
/// </summary>
|
||||
/// <param name="request">Port configuration parameters</param>
|
||||
/// <returns>Result of the port open operation</returns>
|
||||
/// <response code="200">Port opened successfully</response>
|
||||
/// <response code="400">Invalid request parameters</response>
|
||||
/// <response code="500">Internal server error</response>
|
||||
[HttpPost("open")]
|
||||
[ProducesResponseType(typeof(PortOperationResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<PortOperationResponse>> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the currently opened serial port
|
||||
/// </summary>
|
||||
/// <returns>Result of the port close operation</returns>
|
||||
/// <response code="200">Port closed successfully</response>
|
||||
/// <response code="500">Internal server error</response>
|
||||
[HttpPost("close")]
|
||||
[ProducesResponseType(typeof(PortOperationResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<PortOperationResponse>> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a CAT command to the connected radio
|
||||
/// </summary>
|
||||
/// <param name="request">CAT command to send</param>
|
||||
/// <returns>Result of the command operation including any response from the radio</returns>
|
||||
/// <response code="200">Command sent successfully</response>
|
||||
/// <response code="400">Invalid request parameters or no port open</response>
|
||||
/// <response code="500">Internal server error</response>
|
||||
[HttpPost("command")]
|
||||
[ProducesResponseType(typeof(CommandResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<CommandResponse>> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current status of the serial port connection
|
||||
/// </summary>
|
||||
/// <returns>Current port status</returns>
|
||||
/// <response code="200">Returns current port status</response>
|
||||
[HttpGet("status")]
|
||||
[ProducesResponseType(typeof(PortOperationResponse), StatusCodes.Status200OK)]
|
||||
public ActionResult<PortOperationResponse> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
Server/SharpCAT.Server/Models/ApiModels.cs
Normal file
143
Server/SharpCAT.Server/Models/ApiModels.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO.Ports;
|
||||
|
||||
namespace SharpCAT.Server.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Response model for listing available serial ports
|
||||
/// </summary>
|
||||
public class PortListResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Array of available serial port names
|
||||
/// </summary>
|
||||
public string[] Ports { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Number of available ports
|
||||
/// </summary>
|
||||
public int Count => Ports.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for opening a serial port
|
||||
/// </summary>
|
||||
public class OpenPortRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the serial port to open (e.g., "COM1", "/dev/ttyUSB0")
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string PortName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Baud rate for communication (default: 9600)
|
||||
/// </summary>
|
||||
public int BaudRate { get; set; } = 9600;
|
||||
|
||||
/// <summary>
|
||||
/// Parity setting (default: None)
|
||||
/// </summary>
|
||||
public Parity Parity { get; set; } = Parity.None;
|
||||
|
||||
/// <summary>
|
||||
/// Stop bits setting (default: One)
|
||||
/// </summary>
|
||||
public StopBits StopBits { get; set; } = StopBits.One;
|
||||
|
||||
/// <summary>
|
||||
/// Handshake setting (default: None)
|
||||
/// </summary>
|
||||
public Handshake Handshake { get; set; } = Handshake.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for port operations
|
||||
/// </summary>
|
||||
public class PortOperationResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if the operation was successful
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable message about the operation
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the port that was operated on
|
||||
/// </summary>
|
||||
public string? PortName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current status of the port (open/closed)
|
||||
/// </summary>
|
||||
public bool IsOpen { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for sending CAT commands
|
||||
/// </summary>
|
||||
public class SendCommandRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// CAT command string to send to the radio
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Command { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for CAT command operations
|
||||
/// </summary>
|
||||
public class CommandResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if the command was sent successfully
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The command that was sent
|
||||
/// </summary>
|
||||
public string Command { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Response received from the radio (if any)
|
||||
/// </summary>
|
||||
public string? Response { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable message about the operation
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the command was executed
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for errors
|
||||
/// </summary>
|
||||
public class ErrorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Error message
|
||||
/// </summary>
|
||||
public string Error { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Additional details about the error
|
||||
/// </summary>
|
||||
public string? Details { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the error occurred
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
67
Server/SharpCAT.Server/Program.cs
Normal file
67
Server/SharpCAT.Server/Program.cs
Normal file
|
|
@ -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<ISerialCommunicationService, SerialCommunicationService>();
|
||||
|
||||
// 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();
|
||||
31
Server/SharpCAT.Server/Properties/launchSettings.json
Normal file
31
Server/SharpCAT.Server/Properties/launchSettings.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
150
Server/SharpCAT.Server/README.md
Normal file
150
Server/SharpCAT.Server/README.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using System.IO.Ports;
|
||||
|
||||
namespace SharpCAT.Server.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for serial communication services
|
||||
/// </summary>
|
||||
public interface ISerialCommunicationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all available serial ports on the system
|
||||
/// </summary>
|
||||
/// <returns>Array of port names</returns>
|
||||
string[] GetAvailablePorts();
|
||||
|
||||
/// <summary>
|
||||
/// Opens and configures a serial port
|
||||
/// </summary>
|
||||
/// <param name="portName">Name of the port to open</param>
|
||||
/// <param name="baudRate">Baud rate for communication</param>
|
||||
/// <param name="parity">Parity setting</param>
|
||||
/// <param name="stopBits">Stop bits setting</param>
|
||||
/// <param name="handshake">Handshake setting</param>
|
||||
/// <returns>True if port opened successfully</returns>
|
||||
Task<bool> OpenPortAsync(string portName, int baudRate, Parity parity, StopBits stopBits, Handshake handshake);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the currently opened port
|
||||
/// </summary>
|
||||
Task ClosePortAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Sends a CAT command to the radio
|
||||
/// </summary>
|
||||
/// <param name="command">CAT command string to send</param>
|
||||
/// <returns>Response from the radio, if any</returns>
|
||||
Task<string> SendCommandAsync(string command);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of the current port connection
|
||||
/// </summary>
|
||||
/// <returns>True if port is open and connected</returns>
|
||||
bool IsPortOpen();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the currently opened port
|
||||
/// </summary>
|
||||
/// <returns>Port name or null if no port is open</returns>
|
||||
string? GetCurrentPortName();
|
||||
}
|
||||
}
|
||||
208
Server/SharpCAT.Server/Services/SerialCommunicationService.cs
Normal file
208
Server/SharpCAT.Server/Services/SerialCommunicationService.cs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
using System.IO.Ports;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCAT.Server.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for managing serial communication with radios using SharpCAT library
|
||||
/// </summary>
|
||||
public class SerialCommunicationService : ISerialCommunicationService, IDisposable
|
||||
{
|
||||
private readonly ILogger<SerialCommunicationService> _logger;
|
||||
private Serial? _serialConnection;
|
||||
private SerialPort? _directSerialPort;
|
||||
private string? _currentPortName;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private bool _disposed = false;
|
||||
|
||||
public SerialCommunicationService(ILogger<SerialCommunicationService> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting available serial ports");
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ClosePortAsync()
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await ClosePortInternalAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPortOpen()
|
||||
{
|
||||
return _directSerialPort?.IsOpen == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Server/SharpCAT.Server/SharpCAT.Server.csproj
Normal file
20
Server/SharpCAT.Server/SharpCAT.Server.csproj
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.18" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SharpCAT\SharpCAT.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
Server/SharpCAT.Server/SharpCAT.Server.http
Normal file
6
Server/SharpCAT.Server/SharpCAT.Server.http
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@SharpCAT.Server_HostAddress = http://localhost:5188
|
||||
|
||||
GET {{SharpCAT.Server_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
8
Server/SharpCAT.Server/appsettings.Development.json
Normal file
8
Server/SharpCAT.Server/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Server/SharpCAT.Server/appsettings.json
Normal file
9
Server/SharpCAT.Server/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
33
SharpCAT.sln
Normal file
33
SharpCAT.sln
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue