Aggiungere i file di progetto.

This commit is contained in:
Andrea Santaniello 2024-10-27 17:44:38 +01:00
parent 6c675ed097
commit cbfafc4252
7 changed files with 1215 additions and 0 deletions

19
RadioGUI/Program.cs Normal file
View File

@ -0,0 +1,19 @@
using RadioControllerApp;
namespace RadioGUI
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new FormRadio());
}
}
}

615
RadioGUI/Radio.cs Normal file
View File

@ -0,0 +1,615 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using NAudio.Wave;
namespace RadioControllerApp
{
public class FormRadio : Form
{
private RadioController radioController;
private WaveInEvent waveIn;
private BufferedWaveProvider receivedAudioBuffer;
private WaveOutEvent waveOut;
private bool isRecording = false;
// UI Controls
private TextBox txtPortName;
private Button btnOpenConnection;
private Button btnCloseConnection;
private Button btnInitialize;
private TextBox txtTXFrequency;
private TextBox txtRXFrequency;
private TextBox txtTone;
private TextBox txtSquelchLevel;
private Button btnTune;
private CheckBox chkEmphasis;
private CheckBox chkHighpass;
private CheckBox chkLowpass;
private Button btnSetFilters;
private Button btnStartRX;
private Button btnStartTX;
private Button btnEndTX;
private Button btnStop;
private Button btnStartRecording;
private Button btnStopRecording;
private Button btnPlayReceivedAudio;
private TextBox txtStatus;
public FormRadio()
{
InitializeComponent();
InitializeRadioController();
}
private void InitializeComponent()
{
this.AutoScaleMode = AutoScaleMode.Dpi; // Enable DPI scaling
// Form properties
this.Text = "Radio Controller";
this.Size = new System.Drawing.Size(800, 700);
this.MinimumSize = new System.Drawing.Size(600, 6<s00);
this.FormClosing += FormRadio_FormClosing;
// Initialize controls
InitializeControls();
}
private void InitializeControls()
{
// Main TableLayoutPanel
TableLayoutPanel mainLayout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 6,
AutoSize = true,
};
this.Controls.Add(mainLayout);
// Adjust row styles
mainLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // Connection Controls
mainLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // Frequency Controls
mainLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // Filter Controls
mainLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // Mode Controls
mainLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // Audio Controls
mainLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); // Status TextBox
// Connection Controls GroupBox
GroupBox grpConnection = CreateConnectionControls();
grpConnection.Dock = DockStyle.Fill;
mainLayout.Controls.Add(grpConnection, 0, 0);
// Frequency Controls GroupBox
GroupBox grpFrequency = CreateFrequencyControls();
grpFrequency.Dock = DockStyle.Fill;
mainLayout.Controls.Add(grpFrequency, 0, 1);
// Filter Controls GroupBox
GroupBox grpFilters = CreateFilterControls();
grpFilters.Dock = DockStyle.Fill;
mainLayout.Controls.Add(grpFilters, 0, 2);
// Mode Controls GroupBox
GroupBox grpModes = CreateModeControls();
grpModes.Dock = DockStyle.Fill;
mainLayout.Controls.Add(grpModes, 0, 3);
// Audio Controls GroupBox
GroupBox grpAudio = CreateAudioControls();
grpAudio.Dock = DockStyle.Fill;
mainLayout.Controls.Add(grpAudio, 0, 4);
// Status TextBox
txtStatus = new TextBox
{
Multiline = true,
ReadOnly = true,
Dock = DockStyle.Fill,
ScrollBars = ScrollBars.Vertical
};
mainLayout.Controls.Add(txtStatus, 0, 5);
}
private GroupBox CreateConnectionControls()
{
GroupBox grpConnection = new GroupBox
{
Text = "Connection Controls",
AutoSize = true,
Dock = DockStyle.Fill
};
TableLayoutPanel layout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 5,
AutoSize = true
};
grpConnection.Controls.Add(layout);
Label lblPortName = new Label { Text = "Port Name:", Anchor = AnchorStyles.Right };
txtPortName = new TextBox { Text = "COM3", Anchor = AnchorStyles.Left };
btnOpenConnection = new Button { Text = "Open Connection", AutoSize = true };
btnCloseConnection = new Button { Text = "Close Connection", AutoSize = true };
btnInitialize = new Button { Text = "Initialize", AutoSize = true };
btnOpenConnection.Click += btnOpenConnection_Click;
btnCloseConnection.Click += btnCloseConnection_Click;
btnInitialize.Click += btnInitialize_Click;
layout.Controls.Add(lblPortName, 0, 0);
layout.Controls.Add(txtPortName, 1, 0);
layout.Controls.Add(btnOpenConnection, 2, 0);
layout.Controls.Add(btnCloseConnection, 3, 0);
layout.Controls.Add(btnInitialize, 4, 0);
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); // Label
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); // TextBox
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33F)); // Button
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33F)); // Button
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 34F)); // Button
return grpConnection;
}
private GroupBox CreateFrequencyControls()
{
GroupBox grpFrequency = new GroupBox
{
Text = "Frequency Controls",
AutoSize = true,
Dock = DockStyle.Fill
};
TableLayoutPanel layout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 6,
AutoSize = true
};
grpFrequency.Controls.Add(layout);
Label lblTXFrequency = new Label { Text = "TX Frequency:", Anchor = AnchorStyles.Right };
txtTXFrequency = new TextBox { Text = "146.520", Anchor = AnchorStyles.Left };
Label lblRXFrequency = new Label { Text = "RX Frequency:", Anchor = AnchorStyles.Right };
txtRXFrequency = new TextBox { Text = "146.520", Anchor = AnchorStyles.Left };
Label lblTone = new Label { Text = "Tone:", Anchor = AnchorStyles.Right };
txtTone = new TextBox { Text = "00", Anchor = AnchorStyles.Left };
Label lblSquelchLevel = new Label { Text = "Squelch Level:", Anchor = AnchorStyles.Right };
txtSquelchLevel = new TextBox { Text = "0", Anchor = AnchorStyles.Left };
btnTune = new Button { Text = "Tune to Frequency", AutoSize = true };
btnTune.Click += btnTune_Click;
layout.Controls.Add(lblTXFrequency, 0, 0);
layout.Controls.Add(txtTXFrequency, 1, 0);
layout.Controls.Add(lblRXFrequency, 2, 0);
layout.Controls.Add(txtRXFrequency, 3, 0);
layout.Controls.Add(lblTone, 4, 0);
layout.Controls.Add(txtTone, 5, 0);
layout.Controls.Add(lblSquelchLevel, 0, 1);
layout.Controls.Add(txtSquelchLevel, 1, 1);
layout.SetColumnSpan(btnTune, 4);
layout.Controls.Add(btnTune, 2, 1);
// Adjust column styles
for (int i = 0; i < 6; i += 2)
{
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); // Label
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 16.66F)); // TextBox
}
return grpFrequency;
}
private GroupBox CreateFilterControls()
{
GroupBox grpFilters = new GroupBox
{
Text = "Filter Controls",
AutoSize = true,
Dock = DockStyle.Fill
};
FlowLayoutPanel layout = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
WrapContents = false,
};
grpFilters.Controls.Add(layout);
chkEmphasis = new CheckBox { Text = "Emphasis", AutoSize = true };
chkHighpass = new CheckBox { Text = "Highpass", AutoSize = true };
chkLowpass = new CheckBox { Text = "Lowpass", AutoSize = true };
btnSetFilters = new Button { Text = "Set Filters", AutoSize = true };
btnSetFilters.Click += btnSetFilters_Click;
layout.Controls.Add(chkEmphasis);
layout.Controls.Add(chkHighpass);
layout.Controls.Add(chkLowpass);
layout.Controls.Add(btnSetFilters);
return grpFilters;
}
private GroupBox CreateModeControls()
{
GroupBox grpModes = new GroupBox
{
Text = "Mode Controls",
AutoSize = true,
Dock = DockStyle.Fill
};
FlowLayoutPanel layout = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
WrapContents = false,
};
grpModes.Controls.Add(layout);
btnStartRX = new Button { Text = "Start RX Mode", AutoSize = true };
btnStartTX = new Button { Text = "Start TX Mode", AutoSize = true };
btnEndTX = new Button { Text = "End TX Mode", AutoSize = true };
btnStop = new Button { Text = "Stop", AutoSize = true };
btnStartRX.Click += btnStartRX_Click;
btnStartTX.Click += btnStartTX_Click;
btnEndTX.Click += btnEndTX_Click;
btnStop.Click += btnStop_Click;
layout.Controls.Add(btnStartRX);
layout.Controls.Add(btnStartTX);
layout.Controls.Add(btnEndTX);
layout.Controls.Add(btnStop);
return grpModes;
}
private GroupBox CreateAudioControls()
{
GroupBox grpAudio = new GroupBox
{
Text = "Audio Controls",
AutoSize = true,
Dock = DockStyle.Fill
};
FlowLayoutPanel layout = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
WrapContents = false,
};
grpAudio.Controls.Add(layout);
btnStartRecording = new Button { Text = "Start Recording", AutoSize = true };
btnStopRecording = new Button { Text = "Stop Recording", AutoSize = true };
btnPlayReceivedAudio = new Button { Text = "Play Received Audio", AutoSize = true };
btnStartRecording.Click += btnStartRecording_Click;
btnStopRecording.Click += btnStopRecording_Click;
btnPlayReceivedAudio.Click += btnPlayReceivedAudio_Click;
layout.Controls.Add(btnStartRecording);
layout.Controls.Add(btnStopRecording);
layout.Controls.Add(btnPlayReceivedAudio);
return grpAudio;
}
private void InitializeRadioController()
{
// Disable controls that require an open connection
ToggleControls(false);
receivedAudioBuffer = new BufferedWaveProvider(new WaveFormat(44100, 16, 1));
}
private void btnOpenConnection_Click(object sender, EventArgs e)
{
try
{
string portName = txtPortName.Text.Trim();
if (string.IsNullOrEmpty(portName))
{
AppendStatus("Please enter a valid port name.");
return;
}
radioController = new RadioController(portName);
radioController.ErrorOccurred += RadioController_ErrorOccurred;
radioController.AudioDataReceived += RadioController_AudioDataReceived;
radioController.OpenConnection();
AppendStatus($"Connection opened on {portName}.");
ToggleControls(true);
}
catch (Exception ex)
{
AppendStatus($"Error opening connection: {ex.Message}");
}
}
private void btnCloseConnection_Click(object sender, EventArgs e)
{
try
{
if (radioController != null)
{
radioController.CloseConnection();
radioController.Dispose();
radioController = null;
AppendStatus("Connection closed.");
ToggleControls(false);
}
}
catch (Exception ex)
{
AppendStatus($"Error closing connection: {ex.Message}");
}
}
private void btnInitialize_Click(object sender, EventArgs e)
{
try
{
radioController.Initialize();
AppendStatus("Radio initialized.");
}
catch (Exception ex)
{
AppendStatus($"Error initializing radio: {ex.Message}");
}
}
private void btnTune_Click(object sender, EventArgs e)
{
try
{
string txFreq = txtTXFrequency.Text.Trim();
string rxFreq = txtRXFrequency.Text.Trim();
int tone = int.Parse(txtTone.Text.Trim());
int squelch = int.Parse(txtSquelchLevel.Text.Trim());
radioController.TuneToFrequency(txFreq, rxFreq, tone, squelch);
AppendStatus($"Tuned to TX: {txFreq}, RX: {rxFreq}, Tone: {tone}, Squelch Level: {squelch}");
}
catch (Exception ex)
{
AppendStatus($"Error tuning frequency: {ex.Message}");
}
}
private void btnSetFilters_Click(object sender, EventArgs e)
{
try
{
bool emphasis = chkEmphasis.Checked;
bool highpass = chkHighpass.Checked;
bool lowpass = chkLowpass.Checked;
radioController.SetFilters(emphasis, highpass, lowpass);
AppendStatus("Filters set.");
}
catch (Exception ex)
{
AppendStatus($"Error setting filters: {ex.Message}");
}
}
private void btnStartRX_Click(object sender, EventArgs e)
{
try
{
radioController.StartRXMode();
AppendStatus("Started RX mode.");
}
catch (Exception ex)
{
AppendStatus($"Error starting RX mode: {ex.Message}");
}
}
private void btnStartTX_Click(object sender, EventArgs e)
{
try
{
radioController.StartTXMode();
AppendStatus("Started TX mode.");
}
catch (Exception ex)
{
AppendStatus($"Error starting TX mode: {ex.Message}");
}
}
private void btnEndTX_Click(object sender, EventArgs e)
{
try
{
radioController.EndTXMode();
AppendStatus("Ended TX mode.");
}
catch (Exception ex)
{
AppendStatus($"Error ending TX mode: {ex.Message}");
}
}
private void btnStop_Click(object sender, EventArgs e)
{
try
{
radioController.Stop();
AppendStatus("Radio stopped.");
}
catch (Exception ex)
{
AppendStatus($"Error stopping radio: {ex.Message}");
}
}
private void btnStartRecording_Click(object sender, EventArgs e)
{
try
{
if (radioController == null)
{
AppendStatus("Please open the connection first.");
return;
}
radioController.StartTXMode();
isRecording = true;
waveIn = new WaveInEvent();
waveIn.WaveFormat = new WaveFormat(44100, 16, 1);
waveIn.DataAvailable += WaveIn_DataAvailable;
waveIn.StartRecording();
AppendStatus("Recording started.");
}
catch (Exception ex)
{
AppendStatus($"Error starting recording: {ex.Message}");
}
}
private void btnStopRecording_Click(object sender, EventArgs e)
{
try
{
if (isRecording && waveIn != null)
{
waveIn.StopRecording();
waveIn.Dispose();
waveIn = null;
isRecording = false;
radioController.EndTXMode();
AppendStatus("Recording stopped.");
}
}
catch (Exception ex)
{
AppendStatus($"Error stopping recording: {ex.Message}");
}
}
private async void WaveIn_DataAvailable(object sender, WaveInEventArgs e)
{
try
{
if (isRecording && radioController != null)
{
byte[] buffer = new byte[e.BytesRecorded];
Array.Copy(e.Buffer, buffer, e.BytesRecorded);
// Convert audio data to the required format if necessary
await radioController.SendAudioDataAsync(buffer);
}
}
catch (Exception ex)
{
AppendStatus($"Error sending audio data: {ex.Message}");
}
}
private void btnPlayReceivedAudio_Click(object sender, EventArgs e)
{
try
{
if (waveOut == null)
{
waveOut = new WaveOutEvent();
waveOut.Init(receivedAudioBuffer);
}
waveOut.Play();
AppendStatus("Playing received audio.");
}
catch (Exception ex)
{
AppendStatus($"Error playing received audio: {ex.Message}");
}
}
private void RadioController_ErrorOccurred(object sender, ErrorEventArgs e)
{
AppendStatus($"Error: {e.GetException().Message}");
}
private void RadioController_AudioDataReceived(object sender, byte[] data)
{
// Buffer the received audio data
receivedAudioBuffer.AddSamples(data, 0, data.Length);
AppendStatus($"Audio data received. Length: {data.Length} bytes.");
}
private void AppendStatus(string message)
{
if (txtStatus.InvokeRequired)
{
txtStatus.Invoke(new Action(() => AppendStatus(message)));
}
else
{
txtStatus.AppendText($"{DateTime.Now}: {message}{Environment.NewLine}");
}
}
private void ToggleControls(bool isEnabled)
{
btnInitialize.Enabled = isEnabled;
btnTune.Enabled = isEnabled;
btnSetFilters.Enabled = isEnabled;
btnStartRX.Enabled = isEnabled;
btnStartTX.Enabled = isEnabled;
btnEndTX.Enabled = isEnabled;
btnStop.Enabled = isEnabled;
btnStartRecording.Enabled = isEnabled;
btnStopRecording.Enabled = isEnabled;
btnPlayReceivedAudio.Enabled = isEnabled;
// Disable connection buttons appropriately
btnOpenConnection.Enabled = !isEnabled;
btnCloseConnection.Enabled = isEnabled;
}
private void FormRadio_FormClosing(object sender, FormClosingEventArgs e)
{
// Clean up resources
if (radioController != null)
{
radioController.Dispose();
radioController = null;
}
if (waveIn != null)
{
waveIn.Dispose();
waveIn = null;
}
if (waveOut != null)
{
waveOut.Dispose();
waveOut = null;
}
}
}
}

120
RadioGUI/Radio.resx Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

19
RadioGUI/RadioGUI.csproj Normal file
View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NAudio" Version="2.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\kv4p-sharp\kv4p-sharp-lib.csproj" />
</ItemGroup>
</Project>

31
kv4p-sharp.sln Normal file
View File

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35312.102
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kv4p-sharp-lib", "kv4p-sharp\kv4p-sharp-lib.csproj", "{321A4BEA-94AB-4345-91EA-A603180603D4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RadioGUI", "RadioGUI\RadioGUI.csproj", "{833C55A9-5DE4-4B46-8F4A-63EA59325E17}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{321A4BEA-94AB-4345-91EA-A603180603D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{321A4BEA-94AB-4345-91EA-A603180603D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{321A4BEA-94AB-4345-91EA-A603180603D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{321A4BEA-94AB-4345-91EA-A603180603D4}.Release|Any CPU.Build.0 = Release|Any CPU
{833C55A9-5DE4-4B46-8F4A-63EA59325E17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{833C55A9-5DE4-4B46-8F4A-63EA59325E17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{833C55A9-5DE4-4B46-8F4A-63EA59325E17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{833C55A9-5DE4-4B46-8F4A-63EA59325E17}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {19C17713-1168-468F-9761-FAC3257E0E68}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,397 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Ports;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class RadioController : IDisposable
{
private SerialPort serialPort;
private const int baudRate = 921600;
private const int dataBits = 8;
private const Parity parity = Parity.None;
private const StopBits stopBits = StopBits.One;
private const int readTimeout = 1000;
private const int writeTimeout = 1000;
// Delimiter must match ESP32 code
private static readonly byte[] COMMAND_DELIMITER = new byte[]
{
0xFF, 0x00, 0xFF, 0x00,
0xFF, 0x00, 0xFF, 0x00
};
// Command bytes
private enum ESP32Command : byte
{
PTT_DOWN = 1,
PTT_UP = 2,
TUNE_TO = 3,
FILTERS = 4,
STOP = 5,
GET_FIRMWARE_VER = 6
}
private enum RadioMode
{
STARTUP,
RX,
TX,
SCAN
}
private RadioMode currentMode = RadioMode.STARTUP;
private const int MIN_FIRMWARE_VER = 1;
private string versionStrBuffer = "";
private const int AUDIO_SAMPLE_RATE = 44100;
// Synchronization locks
private readonly object _syncLock = new object();
private readonly object _versionStrBufferLock = new object();
/// <summary>
/// Occurs when an error is encountered.
/// </summary>
public event EventHandler<ErrorEventArgs> ErrorOccurred;
/// <summary>
/// Occurs when audio data is received in RX mode.
/// </summary>
public event EventHandler<byte[]> AudioDataReceived;
public RadioController(string portName)
{
if (string.IsNullOrWhiteSpace(portName))
throw new ArgumentException("Port name cannot be null or empty.", nameof(portName));
serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits)
{
ReadTimeout = readTimeout,
WriteTimeout = writeTimeout
};
serialPort.DataReceived += SerialPort_DataReceived;
}
public void OpenConnection()
{
lock (_syncLock)
{
if (!serialPort.IsOpen)
{
serialPort.Open();
}
}
}
public void CloseConnection()
{
lock (_syncLock)
{
if (serialPort.IsOpen)
{
serialPort.Close();
}
}
}
public void Initialize()
{
lock (_syncLock)
{
currentMode = RadioMode.STARTUP;
}
SendCommand(ESP32Command.STOP);
SendCommand(ESP32Command.GET_FIRMWARE_VER);
}
public void StartRXMode()
{
lock (_syncLock)
{
currentMode = RadioMode.RX;
}
}
public void StartTXMode()
{
lock (_syncLock)
{
currentMode = RadioMode.TX;
SendCommand(ESP32Command.PTT_DOWN);
}
}
public void EndTXMode()
{
lock (_syncLock)
{
if (currentMode == RadioMode.TX)
{
SendCommand(ESP32Command.PTT_UP);
currentMode = RadioMode.RX;
}
}
}
public void Stop()
{
lock (_syncLock)
{
currentMode = RadioMode.RX;
}
SendCommand(ESP32Command.STOP);
}
/// <summary>
/// Tunes the radio to the specified frequencies with tone and squelch level.
/// </summary>
/// <param name="txFrequencyStr">Transmit frequency as a string (e.g., "146.520").</param>
/// <param name="rxFrequencyStr">Receive frequency as a string (e.g., "146.520").</param>
/// <param name="tone">Tone value as an integer (00 to 99).</param>
/// <param name="squelchLevel">Squelch level as an integer (0 to 9).</param>
public void TuneToFrequency(string txFrequencyStr, string rxFrequencyStr, int tone, int squelchLevel)
{
if (string.IsNullOrWhiteSpace(txFrequencyStr))
throw new ArgumentException("Transmit frequency cannot be null or empty.", nameof(txFrequencyStr));
if (string.IsNullOrWhiteSpace(rxFrequencyStr))
throw new ArgumentException("Receive frequency cannot be null or empty.", nameof(rxFrequencyStr));
txFrequencyStr = MakeSafe2MFreq(txFrequencyStr);
rxFrequencyStr = MakeSafe2MFreq(rxFrequencyStr);
string toneStr = tone.ToString("00");
string squelchStr = squelchLevel.ToString();
// Ensure squelch level is a single digit
if (squelchStr.Length != 1)
throw new ArgumentException("Squelch level must be a single digit (0-9).", nameof(squelchLevel));
// Build parameters string
string paramsStr = txFrequencyStr + rxFrequencyStr + toneStr + squelchStr;
SendCommand(ESP32Command.TUNE_TO, paramsStr);
}
public void SetFilters(bool emphasis, bool highpass, bool lowpass)
{
string paramsStr = (emphasis ? "1" : "0") + (highpass ? "1" : "0") + (lowpass ? "1" : "0");
SendCommand(ESP32Command.FILTERS, paramsStr);
}
public async Task SendAudioDataAsync(byte[] audioData, CancellationToken cancellationToken = default)
{
lock (_syncLock)
{
if (currentMode != RadioMode.TX)
{
return;
}
}
int chunkSize = 512;
Stopwatch stopwatch = new Stopwatch();
for (int i = 0; i < audioData.Length; i += chunkSize)
{
cancellationToken.ThrowIfCancellationRequested();
int remaining = audioData.Length - i;
int size = remaining > chunkSize ? chunkSize : remaining;
byte[] chunk = new byte[size];
Array.Copy(audioData, i, chunk, 0, size);
stopwatch.Restart();
SendBytesToESP32(chunk);
stopwatch.Stop();
// Calculate the expected delay
double expectedDelay = (double)size / AUDIO_SAMPLE_RATE * 1000.0; // in milliseconds
double elapsedTime = stopwatch.Elapsed.TotalMilliseconds;
int delay = (int)(expectedDelay - elapsedTime);
if (delay > 0)
{
await Task.Delay(delay, cancellationToken);
}
}
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
byte[] receivedData = null;
lock (_syncLock)
{
try
{
int bytesToRead = serialPort.BytesToRead;
receivedData = new byte[bytesToRead];
serialPort.Read(receivedData, 0, bytesToRead);
}
catch (Exception ex)
{
OnErrorOccurred(new ErrorEventArgs(ex));
}
}
if (receivedData != null && receivedData.Length > 0)
{
HandleData(receivedData);
}
}
private void HandleData(byte[] data)
{
RadioMode mode;
lock (_syncLock)
{
mode = currentMode;
}
if (mode == RadioMode.RX)
{
// Raise an event with the received audio data
OnAudioDataReceived(data);
}
else if (mode == RadioMode.STARTUP)
{
// Handle firmware version check
string dataStr = System.Text.Encoding.UTF8.GetString(data);
lock (_versionStrBufferLock)
{
versionStrBuffer += dataStr;
if (versionStrBuffer.Contains("VERSION"))
{
int startIdx = versionStrBuffer.IndexOf("VERSION") + "VERSION".Length;
if (versionStrBuffer.Length >= startIdx + 8)
{
string verStr = versionStrBuffer.Substring(startIdx, 8);
if (int.TryParse(verStr, out int verInt))
{
if (verInt < MIN_FIRMWARE_VER)
{
OnErrorOccurred(new ErrorEventArgs(new InvalidOperationException("Unsupported firmware version.")));
}
else
{
lock (_syncLock)
{
currentMode = RadioMode.RX;
}
// No need to initialize audio playback
}
}
else
{
OnErrorOccurred(new ErrorEventArgs(new FormatException("Invalid firmware version format.")));
}
versionStrBuffer = string.Empty;
}
}
}
}
}
private void SendCommand(ESP32Command command)
{
byte[] commandArray = new byte[COMMAND_DELIMITER.Length + 1];
Array.Copy(COMMAND_DELIMITER, commandArray, COMMAND_DELIMITER.Length);
commandArray[COMMAND_DELIMITER.Length] = (byte)command;
SendBytesToESP32(commandArray);
}
private void SendCommand(ESP32Command command, string paramsStr)
{
byte[] paramsBytes = System.Text.Encoding.ASCII.GetBytes(paramsStr);
byte[] commandArray = new byte[COMMAND_DELIMITER.Length + 1 + paramsBytes.Length];
Array.Copy(COMMAND_DELIMITER, commandArray, COMMAND_DELIMITER.Length);
commandArray[COMMAND_DELIMITER.Length] = (byte)command;
Array.Copy(paramsBytes, 0, commandArray, COMMAND_DELIMITER.Length + 1, paramsBytes.Length);
SendBytesToESP32(commandArray);
}
private void SendBytesToESP32(byte[] data)
{
lock (_syncLock)
{
if (serialPort.IsOpen)
{
try
{
serialPort.Write(data, 0, data.Length);
}
catch (TimeoutException ex)
{
OnErrorOccurred(new ErrorEventArgs(ex));
}
catch (Exception ex)
{
OnErrorOccurred(new ErrorEventArgs(ex));
}
}
}
}
private string MakeSafe2MFreq(string strFreq)
{
// Implement frequency validation and formatting as needed
if (!float.TryParse(strFreq, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float freq))
{
freq = 146.520f; // Default frequency
}
while (freq > 148.0f)
{
freq /= 10f;
}
freq = Math.Min(freq, 148.0f);
freq = Math.Max(freq, 144.0f);
string formattedFreq = freq.ToString("000.000", System.Globalization.CultureInfo.InvariantCulture);
return formattedFreq;
}
protected virtual void OnErrorOccurred(ErrorEventArgs e)
{
ErrorOccurred?.Invoke(this, e);
}
protected virtual void OnAudioDataReceived(byte[] data)
{
AudioDataReceived?.Invoke(this, data);
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
lock (_syncLock)
{
CloseConnection();
serialPort?.Dispose();
serialPort = null;
}
}
disposedValue = true;
}
}
~RadioController()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>kv4p_sharp</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>
</Project>