From cbfafc4252a597f975c1fc9349d14e5cf49534d9 Mon Sep 17 00:00:00 2001 From: Andrea Santaniello Date: Sun, 27 Oct 2024 17:44:38 +0100 Subject: [PATCH] Aggiungere i file di progetto. --- RadioGUI/Program.cs | 19 + RadioGUI/Radio.cs | 615 +++++++++++++++++++++++++++++++ RadioGUI/Radio.resx | 120 ++++++ RadioGUI/RadioGUI.csproj | 19 + kv4p-sharp.sln | 31 ++ kv4p-sharp/RadioController.cs | 397 ++++++++++++++++++++ kv4p-sharp/kv4p-sharp-lib.csproj | 14 + 7 files changed, 1215 insertions(+) create mode 100644 RadioGUI/Program.cs create mode 100644 RadioGUI/Radio.cs create mode 100644 RadioGUI/Radio.resx create mode 100644 RadioGUI/RadioGUI.csproj create mode 100644 kv4p-sharp.sln create mode 100644 kv4p-sharp/RadioController.cs create mode 100644 kv4p-sharp/kv4p-sharp-lib.csproj diff --git a/RadioGUI/Program.cs b/RadioGUI/Program.cs new file mode 100644 index 0000000..5ffb4a7 --- /dev/null +++ b/RadioGUI/Program.cs @@ -0,0 +1,19 @@ +using RadioControllerApp; + +namespace RadioGUI +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [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()); + } + } +} \ No newline at end of file diff --git a/RadioGUI/Radio.cs b/RadioGUI/Radio.cs new file mode 100644 index 0000000..30412a5 --- /dev/null +++ b/RadioGUI/Radio.cs @@ -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 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; + } + } + } +} diff --git a/RadioGUI/Radio.resx b/RadioGUI/Radio.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/RadioGUI/Radio.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/RadioGUI/RadioGUI.csproj b/RadioGUI/RadioGUI.csproj new file mode 100644 index 0000000..8996f4a --- /dev/null +++ b/RadioGUI/RadioGUI.csproj @@ -0,0 +1,19 @@ + + + + WinExe + net8.0-windows + enable + true + enable + + + + + + + + + + + \ No newline at end of file diff --git a/kv4p-sharp.sln b/kv4p-sharp.sln new file mode 100644 index 0000000..38da27e --- /dev/null +++ b/kv4p-sharp.sln @@ -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 diff --git a/kv4p-sharp/RadioController.cs b/kv4p-sharp/RadioController.cs new file mode 100644 index 0000000..c215ffd --- /dev/null +++ b/kv4p-sharp/RadioController.cs @@ -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(); + + /// + /// Occurs when an error is encountered. + /// + public event EventHandler ErrorOccurred; + + /// + /// Occurs when audio data is received in RX mode. + /// + public event EventHandler 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); + } + + /// + /// Tunes the radio to the specified frequencies with tone and squelch level. + /// + /// Transmit frequency as a string (e.g., "146.520"). + /// Receive frequency as a string (e.g., "146.520"). + /// Tone value as an integer (00 to 99). + /// Squelch level as an integer (0 to 9). + 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 +} + diff --git a/kv4p-sharp/kv4p-sharp-lib.csproj b/kv4p-sharp/kv4p-sharp-lib.csproj new file mode 100644 index 0000000..8903659 --- /dev/null +++ b/kv4p-sharp/kv4p-sharp-lib.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + kv4p_sharp + enable + enable + + + + + + +