G26-Telemetry-Software/Radio/App_radio/RadioMain.cs
Álvaro Alcántara Ramírez f0db3a8aeb Radio subida
2026-08-03 21:38:32 +02:00

458 lines
13 KiB
C#

using System.IO.Ports;
namespace App_radio;
public partial class RadioMain : Form
{
private readonly RadioController _radioController = new();
private bool _pilotConfirmed;
private bool _disconnectingAfterFault;
private bool _closing;
private bool _closeReady;
private bool _pttTransitionInProgress;
public RadioMain()
{
InitializeComponent();
ConfigureControls();
SubscribeEvents();
LoadPorts();
ApplyVolumes();
ShowDisconnectedState();
AddLog("Aplicación iniciada.");
}
private void ConfigureControls()
{
PuertoBox.DropDownStyle = ComboBoxStyle.DropDownList;
VolumenBoxUpdown.Minimum = 0;
VolumenBoxUpdown.Maximum = 100;
VolumenBoxUpdown.Increment = 1;
VolumenPilotoUpdown.Minimum = 0;
VolumenPilotoUpdown.Maximum = 100;
VolumenPilotoUpdown.Increment = 1;
PttButton.Enabled = false;
PttButton.Cursor = Cursors.Hand;
}
private void SubscribeEvents()
{
ActualizarButton.Click += ActualizarButton_Click;
ConectarButton.Click += ConectarButton_Click;
PttButton.MouseDown += PttButton_MouseDown;
PttButton.MouseUp += PttButton_MouseUp;
PttButton.MouseCaptureChanged += PttButton_MouseCaptureChanged;
VolumenBoxUpdown.ValueChanged += VolumeControl_Changed;
VolumenPilotoUpdown.ValueChanged += VolumeControl_Changed;
SilenciarBoxCheck.CheckedChanged += VolumeControl_Changed;
SilenciarPilotoCheck.CheckedChanged += VolumeControl_Changed;
Deactivate += RadioMain_Deactivate;
FormClosing += RadioMain_FormClosing;
_radioController.Error += RadioController_Error;
_radioController.PilotAudioReceived += RadioController_PilotAudioReceived;
_radioController.ConnectionFaulted += RadioController_ConnectionFaulted;
}
private void LoadPorts()
{
string? previousSelection = PuertoBox.SelectedItem?.ToString();
PuertoBox.Items.Clear();
string[] availablePorts = SerialPort.GetPortNames();
Array.Sort(availablePorts, ComparePortNames);
PuertoBox.Items.AddRange(availablePorts);
if (previousSelection is not null && PuertoBox.Items.Contains(previousSelection))
{
PuertoBox.SelectedItem = previousSelection;
}
else if (PuertoBox.Items.Count > 0)
{
PuertoBox.SelectedIndex = 0;
}
if (PuertoBox.Items.Count == 0)
{
AddLog("No se ha encontrado ningún puerto COM.");
}
}
private static int ComparePortNames(string? left, string? right)
{
int leftNumber = ExtractPortNumber(left);
int rightNumber = ExtractPortNumber(right);
int numberComparison = leftNumber.CompareTo(rightNumber);
return numberComparison != 0
? numberComparison
: string.Compare(left, right, StringComparison.OrdinalIgnoreCase);
}
private static int ExtractPortNumber(string? portName)
{
if (portName is not null
&& portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase)
&& int.TryParse(portName.AsSpan(3), out int number))
{
return number;
}
return int.MaxValue;
}
private void ActualizarButton_Click(object? sender, EventArgs e)
{
LoadPorts();
AddLog($"Puertos actualizados: {PuertoBox.Items.Count} encontrado(s).");
}
private async void ConectarButton_Click(object? sender, EventArgs e)
{
if (_radioController.IsConnected)
{
await DisconnectRadioAsync("Desconectado por el usuario.");
return;
}
string? selectedPort = PuertoBox.SelectedItem?.ToString();
if (string.IsNullOrWhiteSpace(selectedPort))
{
MessageBox.Show(
this,
"Selecciona un puerto COM antes de conectar.",
"Puerto no seleccionado",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return;
}
SetConnectionControlsEnabled(false);
SetStatus(ConectadoBoxLabel, "● Conectando...", Color.DarkOrange);
SetGeneralRadioStatus("● Conectando...", Color.DarkOrange);
AddLog($"Abriendo {selectedPort} a 115200 baudios...");
try
{
await Task.Run(() => _radioController.Connect(selectedPort));
_pilotConfirmed = false;
SetStatus(ConectadoBoxLabel, "● Conectado", Color.Green);
SetStatus(ConectadoPilotoLabel, "● Sin confirmar", Color.Gray);
SetGeneralRadioStatus("● Escuchando", Color.Green);
ConectarButton.Text = "Desconectar";
PttButton.Enabled = true;
PuertoBox.Enabled = false;
ActualizarButton.Enabled = false;
ConectarButton.Enabled = true;
ApplyVolumes();
AddLog($"Radio del box conectada en {selectedPort}.");
AddLog("Mantén pulsado el botón PTT para hablar.");
}
catch (Exception ex)
{
ShowDisconnectedState();
AddLog($"No se pudo conectar: {ex.Message}");
MessageBox.Show(
this,
$"No se pudo iniciar la radio.\n\n{ex.Message}",
"Error de conexión",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
private void PttButton_MouseDown(object? sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left || !_radioController.IsConnected)
{
return;
}
PttButton.Capture = true;
if (_radioController.SetPtt(true))
{
PttButton.Text = "HABLANDO...\nEscucha simultánea activa";
PttButton.UseVisualStyleBackColor = false;
PttButton.BackColor = Color.IndianRed;
SetGeneralRadioStatus("● Transmitiendo y escuchando", Color.Red);
AddLog("PTT activado: transmitiendo y manteniendo activa la escucha.");
}
}
private void PttButton_MouseUp(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
StopPtt();
}
}
private void PttButton_MouseCaptureChanged(object? sender, EventArgs e)
{
if (!PttButton.Capture && _radioController.IsPttActive)
{
StopPtt();
}
}
private void RadioMain_Deactivate(object? sender, EventArgs e)
{
StopPtt();
}
private void StopPtt()
{
if (_pttTransitionInProgress)
{
return;
}
_pttTransitionInProgress = true;
try
{
bool wasActive = _radioController.IsPttActive;
// El controlador corta la transmisión de forma inmediata y el
// servicio serie deja de enviar al terminar como máximo el fragmento
// actual de 200 bytes.
_radioController.SetPtt(false);
PttButton.Capture = false;
PttButton.Text = "Pulsar para hablar";
PttButton.UseVisualStyleBackColor = true;
PttButton.BackColor = SystemColors.Control;
if (_radioController.IsConnected)
{
SetGeneralRadioStatus("● Escuchando", Color.Green);
}
else
{
SetGeneralRadioStatus("● Desconectada", Color.Gray);
}
// Fuerza el repintado antes de añadir el registro, para que el botón
// nunca parezca quedarse pulsado mientras se actualiza el RichTextBox.
PttButton.Invalidate();
PttButton.Update();
if (wasActive)
{
AddLog("PTT desactivado: escuchando la radio del piloto.");
}
}
finally
{
_pttTransitionInProgress = false;
}
}
private void VolumeControl_Changed(object? sender, EventArgs e)
{
ApplyVolumes();
}
private void ApplyVolumes()
{
int boxPercent = Decimal.ToInt32(VolumenBoxUpdown.Value);
int pilotPercent = Decimal.ToInt32(VolumenPilotoUpdown.Value);
_radioController.SetBoxVolume(boxPercent, SilenciarBoxCheck.Checked);
_radioController.SetPilotVolume(pilotPercent, SilenciarPilotoCheck.Checked);
}
private void RadioController_PilotAudioReceived(object? sender, EventArgs e)
{
RunOnUiThread(() =>
{
if (_pilotConfirmed)
{
return;
}
_pilotConfirmed = true;
SetStatus(ConectadoPilotoLabel, "● Conectado", Color.Green);
AddLog("Se ha recibido un frame de audio válido de la radio del piloto.");
});
}
private void RadioController_Error(object? sender, string message)
{
AddLog(message);
}
private void RadioController_ConnectionFaulted(object? sender, EventArgs e)
{
RunOnUiThread(async () =>
{
if (_disconnectingAfterFault || !_radioController.IsConnected)
{
return;
}
_disconnectingAfterFault = true;
try
{
await DisconnectRadioAsync("La conexión se ha cerrado por un error.");
}
finally
{
_disconnectingAfterFault = false;
}
});
}
private async Task DisconnectRadioAsync(string logMessage)
{
StopPtt();
SetConnectionControlsEnabled(false);
try
{
await _radioController.DisconnectAsync();
}
catch (Exception ex)
{
AddLog($"Error al cerrar la conexión: {ex.Message}");
}
finally
{
ShowDisconnectedState();
AddLog(logMessage);
}
}
private void ShowDisconnectedState()
{
_pilotConfirmed = false;
SetStatus(ConectadoBoxLabel, "● Desconectado", Color.Gray);
SetStatus(ConectadoPilotoLabel, "● Desconectado", Color.Gray);
SetGeneralRadioStatus("● Desconectada", Color.Gray);
ConectarButton.Text = "Conectar";
ConectarButton.Enabled = true;
PuertoBox.Enabled = true;
ActualizarButton.Enabled = true;
PttButton.Enabled = false;
PttButton.Text = "Pulsar para hablar";
PttButton.UseVisualStyleBackColor = true;
PttButton.BackColor = SystemColors.Control;
}
private void SetConnectionControlsEnabled(bool enabled)
{
ConectarButton.Enabled = enabled;
PuertoBox.Enabled = enabled;
ActualizarButton.Enabled = enabled;
PttButton.Enabled = enabled && _radioController.IsConnected;
}
private static void SetStatus(Label label, string text, Color color)
{
label.Text = text;
label.ForeColor = color;
}
private void SetGeneralRadioStatus(string text, Color color)
{
label2.Text = text;
label2.ForeColor = color;
}
private void AddLog(string message)
{
RunOnUiThread(() =>
{
if (RegistrosTextBox.TextLength > 100_000)
{
RegistrosTextBox.Select(0, 20_000);
RegistrosTextBox.SelectedText = string.Empty;
}
RegistrosTextBox.AppendText(
$"[{DateTime.Now:HH:mm:ss}] {message}{Environment.NewLine}");
RegistrosTextBox.SelectionStart = RegistrosTextBox.TextLength;
RegistrosTextBox.ScrollToCaret();
});
}
private void RunOnUiThread(Action action)
{
if (IsDisposed || Disposing)
{
return;
}
if (InvokeRequired)
{
try
{
BeginInvoke(action);
}
catch (InvalidOperationException)
{
// La ventana se está cerrando.
}
}
else
{
action();
}
}
private async void RadioMain_FormClosing(object? sender, FormClosingEventArgs e)
{
if (_closeReady)
{
return;
}
e.Cancel = true;
if (_closing)
{
return;
}
_closing = true;
// La ventana desaparece al instante. La limpieza del audio y del puerto
// se realiza después, con un límite de tiempo para que la X nunca deje la
// aplicación bloqueada en pantalla.
StopPtt();
Enabled = false;
Hide();
Task disconnectTask = Task.Run(async () => await _radioController.DisconnectAsync());
try
{
await disconnectTask.WaitAsync(TimeSpan.FromSeconds(2));
}
catch (TimeoutException)
{
// Cerramos igualmente la interfaz. La tarea de limpieza podrá acabar
// en segundo plano sin impedir que termine la aplicación.
}
catch (Exception)
{
// Durante el cierre no mostramos cuadros de error que puedan volver a
// bloquear la salida de la aplicación.
}
_closeReady = true;
Close();
}
}