Adicionado heartbeat comunicação UDP
This commit is contained in:
parent
cf3056c44d
commit
c03c5b1736
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
public enum T_Code
|
||||
{
|
||||
Vzo = -1,
|
||||
Vzo = 0,
|
||||
Imu = 53,
|
||||
Mov = 100,
|
||||
Dir = 101,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ using AgroBase.Services.Operadores;
|
|||
using System.Diagnostics;
|
||||
using Newtonsoft.Json;
|
||||
using AgroMonitor;
|
||||
using System.Net;
|
||||
|
||||
namespace AgroBase.Models
|
||||
{
|
||||
|
|
@ -178,93 +179,130 @@ namespace AgroBase.Models
|
|||
|
||||
|
||||
|
||||
|
||||
public static UdpReliableChannel UdpChannel;
|
||||
private static Action<byte, byte[], IPEndPoint> _onPacketHandler;
|
||||
|
||||
// Thread-safe: guarda o último comando UDP recebido (UTC ticks). 0 = nunca/armado off
|
||||
private static long _lastUdpTicksUtc = 0;
|
||||
// Último pacote UDP recebido (qualquer type, inclusive heartbeat)
|
||||
private static long _lastPacketTicksUtc = 0;
|
||||
|
||||
// Evita reentrância do tick (caso seu timer possa chamar antes do anterior terminar)
|
||||
// Último comando válido recebido (somente type 0x01 + parse ok)
|
||||
private static long _lastValidCmdTicksUtc = 0;
|
||||
|
||||
// Guards anti-reentrância
|
||||
private static int _failSafeTickRunning = 0;
|
||||
private static int _udpWatchdogTickRunning = 0;
|
||||
private static int _udpRestarting = 0;
|
||||
|
||||
private static AsyncTaskTimerModel _tmrFailSafe;
|
||||
private static AsyncTaskTimerModel _tmrUdpWatchdog;
|
||||
|
||||
private const int FAILSAFE_MS = 400;
|
||||
private const int UDP_RESTART_MS = 2000;
|
||||
|
||||
|
||||
public static void IniciarUDP()
|
||||
{
|
||||
StartUdpChannel();
|
||||
|
||||
_tmrFailSafe = new AsyncTaskTimerModel("tmrFailSafe", tmrFailSafe_Tick, 100);
|
||||
_tmrFailSafe.Start();
|
||||
|
||||
_tmrUdpWatchdog = new AsyncTaskTimerModel("tmrUdpWatchdog", tmrUdpWatchdog_Tick, 250);
|
||||
_tmrUdpWatchdog.Start();
|
||||
}
|
||||
|
||||
private static void StartUdpChannel()
|
||||
{
|
||||
try
|
||||
{
|
||||
UdpChannel?.Stop();
|
||||
UdpChannel?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
|
||||
UdpChannel = new UdpReliableChannel();
|
||||
UdpChannel.Start(VariaveisPortas.Ethernet_UDP_RX);
|
||||
|
||||
UdpChannel.OnPacket += (type, payload, from) =>
|
||||
{
|
||||
if (type != 0x01) return;
|
||||
if (!UdpCtrlMessage.TryParse(payload, out var msg)) return;
|
||||
// cria 1 vez e reutiliza a MESMA referência
|
||||
_onPacketHandler = OnUdpPacket;
|
||||
UdpChannel.OnPacket += _onPacketHandler;
|
||||
|
||||
Interlocked.Exchange(ref _lastUdpTicksUtc, DateTime.UtcNow.Ticks);
|
||||
|
||||
// flags globais
|
||||
bool emergencia = (msg.Flags & UdpCtrlFlags.Emergencia) != 0;
|
||||
bool pausa = (msg.Flags & UdpCtrlFlags.Pausa) != 0;
|
||||
|
||||
var disp = (T_Code)msg.Device;
|
||||
|
||||
OperacaoComandoBaseModel controle = new OperacaoComandoBaseModel()
|
||||
{
|
||||
Emergencia = emergencia,
|
||||
Pausa = pausa,
|
||||
Tecla = msg.Key,
|
||||
Dispositivo = disp,
|
||||
};
|
||||
|
||||
// aplica parâmetros por dispositivo (alinhado ao MQTT)
|
||||
if ((msg.Flags & UdpCtrlFlags.HasPayload) != 0)
|
||||
{
|
||||
if (disp == T_Code.Dir)
|
||||
{
|
||||
double angulo = msg.P1 / 100.0;
|
||||
var tipoMov = (TipoMovimentoDirecional)(byte)msg.P2;
|
||||
|
||||
// atualiza seu contexto de controle (igual mqtt faz)
|
||||
controle.Controle.AnguloSP = angulo;
|
||||
controle.Controle.TipoMovimentoDirecional = tipoMov;
|
||||
}
|
||||
else if (disp == T_Code.Mov)
|
||||
{
|
||||
double velPct = msg.P1 / 100.0;
|
||||
controle.Controle.PercentualVelocidadeSP = velPct;
|
||||
}
|
||||
}
|
||||
|
||||
Variaveis.OperacaoEmAndamento.ExecutaComandoDaBase(controle);
|
||||
};
|
||||
|
||||
// Tick 100ms (ok). Se quiser mais responsivo, use 50ms.
|
||||
_tmrFailSafe = new AsyncTaskTimerModel("tmrFailSafe", tmrFailSafe_Tick, 100);
|
||||
_tmrFailSafe.Start();
|
||||
Interlocked.Exchange(ref _lastPacketTicksUtc, DateTime.UtcNow.Ticks);
|
||||
}
|
||||
|
||||
private static void OnUdpPacket(byte type, byte[] payload, IPEndPoint from)
|
||||
{
|
||||
// Qualquer pacote marca link vivo (inclusive heartbeat)
|
||||
Interlocked.Exchange(ref _lastPacketTicksUtc, DateTime.UtcNow.Ticks);
|
||||
|
||||
// Heartbeat: não faz nada além de manter vivo.
|
||||
if (type == UdpReliableChannel.TYPE_HEARTBEAT)
|
||||
{
|
||||
Console.WriteLine("[UDP] Heartbeat");
|
||||
return;
|
||||
}
|
||||
|
||||
// Só controle entra daqui
|
||||
if (type != UdpReliableChannel.TYPE_COMMAND) return;
|
||||
|
||||
if (!UdpCtrlMessage.TryParse(payload, out var msg)) return;
|
||||
|
||||
// Comando válido (controle) atualiza o failsafe
|
||||
Interlocked.Exchange(ref _lastValidCmdTicksUtc, DateTime.UtcNow.Ticks);
|
||||
|
||||
bool emergencia = (msg.Flags & UdpCtrlFlags.Emergencia) != 0;
|
||||
bool pausa = (msg.Flags & UdpCtrlFlags.Pausa) != 0;
|
||||
|
||||
var disp = (T_Code)msg.Device;
|
||||
|
||||
var controle = new OperacaoComandoBaseModel()
|
||||
{
|
||||
Emergencia = emergencia,
|
||||
Pausa = pausa,
|
||||
Tecla = msg.Key,
|
||||
Dispositivo = disp,
|
||||
Controle = new OperacaoComandoBaseControleModel(),
|
||||
};
|
||||
|
||||
if ((msg.Flags & UdpCtrlFlags.HasPayload) != 0)
|
||||
{
|
||||
if (disp == T_Code.Dir)
|
||||
{
|
||||
controle.Controle.AnguloSP = msg.P1 / 100.0;
|
||||
controle.Controle.TipoMovimentoDirecional = (TipoMovimentoDirecional)(byte)msg.P2;
|
||||
}
|
||||
else if (disp == T_Code.Mov)
|
||||
{
|
||||
controle.Controle.PercentualVelocidadeSP = msg.P1 / 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
Variaveis.OperacaoEmAndamento.ExecutaComandoDaBase(controle);
|
||||
}
|
||||
|
||||
// ===== FAILSAFE: só considera comando de controle válido (não heartbeat) =====
|
||||
public static async Task tmrFailSafe_Tick()
|
||||
{
|
||||
// Guard anti-reentrância
|
||||
if (Interlocked.Exchange(ref _failSafeTickRunning, 1) == 1)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
long ticks = Interlocked.Read(ref _lastUdpTicksUtc);
|
||||
long ticks = Interlocked.Read(ref _lastValidCmdTicksUtc);
|
||||
if (ticks == 0) return;
|
||||
|
||||
var last = new DateTime(ticks, DateTimeKind.Utc);
|
||||
double ms = (DateTime.UtcNow - last).TotalMilliseconds;
|
||||
var lastCmd = new DateTime(ticks, DateTimeKind.Utc);
|
||||
double ms = (DateTime.UtcNow - lastCmd).TotalMilliseconds;
|
||||
|
||||
if (ms > FAILSAFE_MS)
|
||||
{
|
||||
// Para movimento e direção por segurança
|
||||
// Para por segurança
|
||||
GeneralJoystick.EnviaComandoMotor(Keys.Escape, T_Code.Mov, ForcarComando: true, ID: null);
|
||||
GeneralJoystick.EnviaComandoMotor(Keys.Escape, T_Code.Dir, ForcarComando: true, ID: null);
|
||||
|
||||
// desarma para não ficar repetindo ESC pra sempre
|
||||
Interlocked.Exchange(ref _lastUdpTicksUtc, 0);
|
||||
// desarma até chegar novo comando válido
|
||||
Interlocked.Exchange(ref _lastValidCmdTicksUtc, 0);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
|
@ -275,6 +313,63 @@ namespace AgroBase.Models
|
|||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ===== WATCHDOG: se parou de chegar qualquer pacote, reinicia o socket =====
|
||||
public static async Task tmrUdpWatchdog_Tick()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _udpWatchdogTickRunning, 1) == 1)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
long ticks = Interlocked.Read(ref _lastPacketTicksUtc);
|
||||
if (ticks == 0) return;
|
||||
|
||||
var lastPkt = new DateTime(ticks, DateTimeKind.Utc);
|
||||
double ms = (DateTime.UtcNow - lastPkt).TotalMilliseconds;
|
||||
|
||||
if (ms > UDP_RESTART_MS)
|
||||
{
|
||||
await RestartUdpChannelSafe();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _udpWatchdogTickRunning, 0);
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static async Task RestartUdpChannelSafe()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _udpRestarting, 1) == 1)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_onPacketHandler != null)
|
||||
UdpChannel.OnPacket -= _onPacketHandler;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Reinicia o canal
|
||||
StartUdpChannel();
|
||||
|
||||
// Importante: zera comando válido, porque “socket caiu” geralmente quebra o fluxo de comando
|
||||
Interlocked.Exchange(ref _lastValidCmdTicksUtc, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _udpRestarting, 0);
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class VariaveisPortas
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AgroBase.Models;
|
||||
using AgroMonitor;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
|
|
@ -9,6 +10,8 @@ using System.Threading.Tasks;
|
|||
public sealed class UdpReliableChannel : IDisposable
|
||||
{
|
||||
public const ushort MAGIC = 0xA711;
|
||||
public const byte TYPE_COMMAND = 0x7F;
|
||||
public const byte TYPE_HEARTBEAT = 0x7F;
|
||||
|
||||
public int AckTimeoutMs { get; set; } = 120;
|
||||
public int MaxPayloadBytes { get; set; } = 1200; // evita fragmentação
|
||||
|
|
@ -29,6 +32,14 @@ public sealed class UdpReliableChannel : IDisposable
|
|||
private readonly ConcurrentDictionary<uint, TaskCompletionSource<bool>> _pendingAck =
|
||||
new ConcurrentDictionary<uint, TaskCompletionSource<bool>>();
|
||||
|
||||
public bool EnableHeartbeat { get; set; } = false;
|
||||
public int HeartbeatIntervalMs { get; set; } = 350;
|
||||
public byte[] HeartbeatPayload { get; set; } = new byte[] { 0 };
|
||||
private CancellationTokenSource _hbCts;
|
||||
private Task _hbTask;
|
||||
private int _hbRunning = 0;
|
||||
|
||||
|
||||
public void SetRemote(string ip, int port)
|
||||
{
|
||||
var addr = IPAddress.Parse(ip);
|
||||
|
|
@ -43,15 +54,119 @@ public sealed class UdpReliableChannel : IDisposable
|
|||
{
|
||||
if (_udp != null) return;
|
||||
|
||||
_seqTx = 0; // opcional
|
||||
_lastSeqRx = 0; // importante
|
||||
_running = true;
|
||||
|
||||
_udp = new UdpClient(localPort);
|
||||
_udp.Client.ReceiveBufferSize = 1 << 20;
|
||||
_udp.Client.SendBufferSize = 1 << 20;
|
||||
|
||||
_running = true;
|
||||
_rxThread = new Thread(RxLoop) { IsBackground = true, Name = "UdpReliableChannel.Rx" };
|
||||
_rxThread.Start();
|
||||
|
||||
if (EnableHeartbeat)
|
||||
StartHeartbeat();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_running = false;
|
||||
|
||||
StopHeartbeat();
|
||||
|
||||
try { _udp?.Close(); } catch { } // força o Receive quebrar
|
||||
try { _udp?.Dispose(); } catch { } // garante liberar socket
|
||||
|
||||
// espera thread encerrar (pra não ficar thread velha viva)
|
||||
try
|
||||
{
|
||||
if (_rxThread != null && _rxThread.IsAlive)
|
||||
{
|
||||
if (!_rxThread.Join(500))
|
||||
{
|
||||
// Se travar, não aborta (Thread.Abort é treta), só segue.
|
||||
// Mas normalmente Close() já libera.
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
_udp = null;
|
||||
_rxThread = null;
|
||||
|
||||
// reseta sequência RX pra evitar descartar tudo após restart
|
||||
_lastSeqRx = 0;
|
||||
|
||||
// opcional: limpa ACKs pendentes pra não vazar
|
||||
foreach (var kv in _pendingAck)
|
||||
kv.Value.TrySetResult(false);
|
||||
_pendingAck.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
private void StartHeartbeat()
|
||||
{
|
||||
if (_hbCts != null) return;
|
||||
|
||||
_hbCts = new CancellationTokenSource();
|
||||
_hbTask = Task.Run(async () =>
|
||||
{
|
||||
while (!_hbCts.IsCancellationRequested)
|
||||
{
|
||||
// evita reentrância se o intervalo for curto e enviar demorar
|
||||
if (Interlocked.Exchange(ref _hbRunning, 1) == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Só manda se tiver started e tiver remote
|
||||
var udp = _udp;
|
||||
var remote = Remote; // snapshot
|
||||
if (udp != null && remote != null)
|
||||
{
|
||||
await SendAsync(TYPE_HEARTBEAT, HeartbeatPayload, requestAck: false).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignora: heartbeat não pode derrubar o canal
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _hbRunning, 0);
|
||||
}
|
||||
}
|
||||
|
||||
try { await Task.Delay(HeartbeatIntervalMs, _hbCts.Token).ConfigureAwait(false); }
|
||||
catch (TaskCanceledException) { }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void StopHeartbeat()
|
||||
{
|
||||
try { _hbCts?.Cancel(); } catch { }
|
||||
|
||||
try
|
||||
{
|
||||
if (_hbTask != null)
|
||||
_hbTask.Wait(200);
|
||||
}
|
||||
catch { }
|
||||
|
||||
try { _hbCts?.Dispose(); } catch { }
|
||||
|
||||
_hbCts = null;
|
||||
_hbTask = null;
|
||||
Interlocked.Exchange(ref _hbRunning, 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public uint NextSeq()
|
||||
{
|
||||
unchecked { return ++_seqTx; }
|
||||
|
|
@ -223,11 +338,5 @@ public sealed class UdpReliableChannel : IDisposable
|
|||
);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_running = false;
|
||||
|
||||
try { _udp?.Close(); } catch { }
|
||||
_udp = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -296,7 +296,8 @@ namespace OperationControl.Controls
|
|||
private void ManualControlPad_EmergencyClicked(object? sender, EventArgs e)
|
||||
{
|
||||
EmergencyActivated = !EmergencyActivated;
|
||||
VariaveisControleOperacao.EnviarComandoParada(emergencia: EmergencyActivated);
|
||||
//VariaveisControleOperacao.EnviarComandoParada(emergencia: EmergencyActivated);
|
||||
_ = VariaveisControleOperacao.EnviarComandoParadaUDP(ipRover: VariaveisControleOperacao.RoverEmFoco?.IP, emergencia: EmergencyActivated);
|
||||
}
|
||||
|
||||
private void ManualControlPad_PauseClicked(object? sender, EventArgs e)
|
||||
|
|
@ -340,7 +341,8 @@ namespace OperationControl.Controls
|
|||
AgroBase.Models.Enums.Direcao.Parado;
|
||||
if (_lastDirKey != dir)
|
||||
{
|
||||
VariaveisControleOperacao.EnviarComandoDirecional(dir, Convert.ToInt32(AngleControl), MoveTypeControl);
|
||||
//VariaveisControleOperacao.EnviarComandoDirecional(dir, Convert.ToInt32(AngleControl), MoveTypeControl);
|
||||
VariaveisControleOperacao.EnviarComandoDirecionalUDP(ipRover: VariaveisControleOperacao.RoverEmFoco?.IP, dir, Convert.ToInt32(AngleControl), MoveTypeControl);
|
||||
_lastDirKey = dir;
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +352,8 @@ namespace OperationControl.Controls
|
|||
AgroBase.Models.Enums.Direcao.Parado;
|
||||
if (_lastMovKey != mov)
|
||||
{
|
||||
VariaveisControleOperacao.EnviarComandoMovimentacao(mov, Convert.ToInt32(SpeedControl));
|
||||
//VariaveisControleOperacao.EnviarComandoMovimentacao(mov, Convert.ToInt32(SpeedControl));
|
||||
VariaveisControleOperacao.EnviarComandoMovimentacaoUDP(ipRover: VariaveisControleOperacao.RoverEmFoco?.IP, mov, Convert.ToInt32(SpeedControl));
|
||||
_lastMovKey = mov;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ namespace OperationControl.Models
|
|||
public static MqttService MqttService;
|
||||
public static GpsService GpsService;
|
||||
public static UdpReliableChannel UdpChannel;
|
||||
public static ManualControlSender ControlSenderDir;
|
||||
public static ManualControlSender ControlSenderMov;
|
||||
|
||||
public static void MostrarLog(string message)
|
||||
{
|
||||
|
|
@ -38,8 +40,10 @@ namespace OperationControl.Models
|
|||
{
|
||||
try
|
||||
{
|
||||
string device_id = message.Mensagem;
|
||||
VariaveisControleOperacao.AdicionarNovoRoverNaRede(message.Mensagem);
|
||||
var p = message.Mensagem.Split(',');
|
||||
string device_id = p[0];
|
||||
string device_ip = p[1];
|
||||
VariaveisControleOperacao.AdicionarNovoRoverNaRede(device_id, device_ip);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -52,8 +56,18 @@ namespace OperationControl.Models
|
|||
|
||||
public static void IniciarUDP()
|
||||
{
|
||||
UdpChannel = new UdpReliableChannel();
|
||||
UdpChannel = new UdpReliableChannel
|
||||
{
|
||||
EnableHeartbeat = true,
|
||||
HeartbeatIntervalMs = 1000,
|
||||
HeartbeatPayload = new byte[] { 0 }
|
||||
};
|
||||
UdpChannel.Start(VariaveisPortas.Ethernet_UDP_TX);
|
||||
|
||||
ControlSenderDir = new ManualControlSender(UdpChannel, (byte)AgroBase.Models.Enums.T_Code.Dir);
|
||||
ControlSenderDir.Start();
|
||||
ControlSenderMov = new ManualControlSender(UdpChannel, (byte)AgroBase.Models.Enums.T_Code.Mov);
|
||||
ControlSenderMov.Start();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -84,7 +98,7 @@ namespace OperationControl.Models
|
|||
public static OperacaoParametrosModel? RoverEmFoco => RoversNaRede.FirstOrDefault(x => x.RoverId == SelectedRoverId);
|
||||
public static bool BaseEmFoco => SelectedRoverId == BaseMarkerID;
|
||||
|
||||
public static async void AdicionarNovoRoverNaRede(string device_id)
|
||||
public static async void AdicionarNovoRoverNaRede(string device_id, string device_ip)
|
||||
{
|
||||
if (AdicionandoNovoRover) return;
|
||||
AdicionandoNovoRover = true;
|
||||
|
|
@ -107,6 +121,7 @@ namespace OperationControl.Models
|
|||
RoversNaRede.Add(new OperacaoParametrosModel()
|
||||
{
|
||||
RoverId = device_id,
|
||||
IP = device_ip,
|
||||
Configurado = false,
|
||||
Alive = true,
|
||||
DadosLeitura = new OperacaoParametrosDadosModel()
|
||||
|
|
@ -166,6 +181,7 @@ namespace OperationControl.Models
|
|||
if (rover != null)
|
||||
{
|
||||
obj.UltimoContato = DateTime.Now;
|
||||
obj.IP = rover.IP;
|
||||
RoversNaRede.Remove(rover);
|
||||
RoversNaRede.Add(obj);
|
||||
}
|
||||
|
|
@ -305,11 +321,12 @@ namespace OperationControl.Models
|
|||
|
||||
public static Task EnviarComandoParadaUDP(string ipRover, bool emergencia = false, bool pausa = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ipRover)) return Task.CompletedTask;
|
||||
Variaveis.UdpChannel.SetRemote(ipRover, VariaveisPortas.Ethernet_UDP_RX);
|
||||
|
||||
var msg = new UdpCtrlMessage
|
||||
{
|
||||
Device = (byte)AgroBase.Models.Enums.T_Code.Mod,
|
||||
Device = (byte)AgroBase.Models.Enums.T_Code.Vzo,
|
||||
Flags = (emergencia ? UdpCtrlFlags.Emergencia : 0) | (pausa ? UdpCtrlFlags.Pausa : 0)
|
||||
};
|
||||
|
||||
|
|
@ -416,9 +433,11 @@ namespace OperationControl.Models
|
|||
});
|
||||
}
|
||||
|
||||
public static Task EnviarComandoDirecionalUDP(string ipRover, Keys key, double angulo, AgroBase.Models.Enums.TipoMovimentoDirecional tipoMov)
|
||||
public static void EnviarComandoDirecionalUDP(string ipRover, AgroBase.Models.Enums.Direcao direcao, double angulo, AgroBase.Models.Enums.TipoMovimentoDirecional tipoMov)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ipRover)) return;
|
||||
Variaveis.UdpChannel.SetRemote(ipRover, VariaveisPortas.Ethernet_UDP_RX);
|
||||
var key = DeParaDirecaoTecla(direcao);
|
||||
|
||||
var msg = new UdpCtrlMessage
|
||||
{
|
||||
|
|
@ -429,7 +448,8 @@ namespace OperationControl.Models
|
|||
P2 = (short)tipoMov
|
||||
};
|
||||
|
||||
return Variaveis.UdpChannel.SendBurstAsync(0x01, msg.ToBytes(), count: 6, intervalMs: 25, requestAck: false);
|
||||
//return Variaveis.UdpChannel.SendBurstAsync(0x01, msg.ToBytes(), count: 6, intervalMs: 25, requestAck: false);
|
||||
Variaveis.ControlSenderDir.Update(msg);
|
||||
}
|
||||
|
||||
public static void EnviarComandoMovimentacao(AgroBase.Models.Enums.Direcao direcao, double velocidade)
|
||||
|
|
@ -446,9 +466,11 @@ namespace OperationControl.Models
|
|||
});
|
||||
}
|
||||
|
||||
public static Task EnviarComandoMovimentacaoUDP(string ipRover, Keys key, double velPct)
|
||||
public static void EnviarComandoMovimentacaoUDP(string ipRover, AgroBase.Models.Enums.Direcao direcao, double velPct)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ipRover)) return;
|
||||
Variaveis.UdpChannel.SetRemote(ipRover, VariaveisPortas.Ethernet_UDP_RX);
|
||||
var key = DeParaDirecaoTecla(direcao);
|
||||
|
||||
var msg = new UdpCtrlMessage
|
||||
{
|
||||
|
|
@ -459,7 +481,8 @@ namespace OperationControl.Models
|
|||
P2 = 0
|
||||
};
|
||||
|
||||
return Variaveis.UdpChannel.SendBurstAsync(0x01, msg.ToBytes(), count: 6, intervalMs: 25, requestAck: false);
|
||||
//return Variaveis.UdpChannel.SendBurstAsync(0x01, msg.ToBytes(), count: 6, intervalMs: 25, requestAck: false);
|
||||
Variaveis.ControlSenderMov.Update(msg);
|
||||
}
|
||||
|
||||
public static void EnviarComandoAtuador(string componente_id, bool? status = null, int? angulo_controle = null, double? angulo_abertura = null, double? altura = null)
|
||||
|
|
|
|||
|
|
@ -1,77 +1,125 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AgroBase;
|
||||
using AgroBase.Models;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
public sealed class ManualControlSender : IDisposable
|
||||
{
|
||||
private readonly UdpReliableChannel _udp;
|
||||
private readonly byte _device; // ex: (byte)T_Code.Mov ou T_Code.Dir
|
||||
private readonly HashSet<Keys> _pressed = new();
|
||||
private readonly byte _device;
|
||||
private readonly object _lock = new();
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _loop;
|
||||
|
||||
private bool _armed;
|
||||
private UdpCtrlMessage? _currentMsg;
|
||||
|
||||
// Ajuste fino
|
||||
public int RepeatIntervalMs { get; set; } = 250; // 4 Hz
|
||||
public int BurstDownCount { get; set; } = 6;
|
||||
public int BurstDownIntervalMs { get; set; } = 25;
|
||||
public int BurstStartCount { get; set; } = 6;
|
||||
public int BurstStartIntervalMs { get; set; } = 25;
|
||||
public int BurstStopCount { get; set; } = 10;
|
||||
public int BurstStopIntervalMs { get; set; } = 25;
|
||||
|
||||
// protocolo: type 0x01 = KeyCmd
|
||||
private const byte TYPE_KEY_CMD = 0x01;
|
||||
// anti-spam de stop
|
||||
private DateTime _lastStopSentUtc = DateTime.MinValue;
|
||||
public int MinStopIntervalMs { get; set; } = 150;
|
||||
|
||||
// Deadman por falha de envio (não depende de Update)
|
||||
// Se ficar esse tempo sem conseguir mandar nada enquanto armado, ele solta e manda STOP.
|
||||
public int SendFailDeadmanMs { get; set; } = 1500; // 0 desliga
|
||||
private DateTime _lastSendOkUtc = DateTime.MinValue;
|
||||
|
||||
public ManualControlSender(UdpReliableChannel udp, byte device)
|
||||
{
|
||||
_udp = udp;
|
||||
_udp = udp ?? throw new ArgumentNullException(nameof(udp));
|
||||
_device = device;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_cts != null) return;
|
||||
_cts = new CancellationTokenSource();
|
||||
_loop = Task.Run(() => Loop(_cts.Token));
|
||||
}
|
||||
|
||||
public void KeyDown(Keys key)
|
||||
/// <summary>
|
||||
/// Chame apenas quando o comando mudou (apertou / soltou / trocou direção).
|
||||
/// O loop cuida do reenvio contínuo.
|
||||
/// </summary>
|
||||
public void Update(UdpCtrlMessage msg)
|
||||
{
|
||||
if (!IsManualKey(key)) return;
|
||||
if (msg == null) return;
|
||||
|
||||
lock (_lock) _pressed.Add(key);
|
||||
msg.Device = _device;
|
||||
|
||||
// burst imediato pra resposta rápida
|
||||
_ = SendKeyBurstAsync(key, BurstDownCount, BurstDownIntervalMs);
|
||||
}
|
||||
bool isNeutral = IsNeutral(msg);
|
||||
|
||||
public void KeyUp(Keys key)
|
||||
{
|
||||
if (!IsManualKey(key)) return;
|
||||
lock (_lock)
|
||||
{
|
||||
if (isNeutral)
|
||||
{
|
||||
_armed = false;
|
||||
_currentMsg = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_armed = true;
|
||||
_currentMsg = msg;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_lock) _pressed.Remove(key);
|
||||
|
||||
// sempre que soltar algo, manda STOP (Esc) burst
|
||||
_ = SendKeyBurstAsync(Keys.Escape, BurstStopCount, BurstStopIntervalMs);
|
||||
// fora do lock
|
||||
if (isNeutral)
|
||||
_ = SendStopBurstIfNeededAsync();
|
||||
else
|
||||
_ = SendBurstAsync(msg, BurstStartCount, BurstStartIntervalMs);
|
||||
}
|
||||
|
||||
private async Task Loop(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
Keys? active = null;
|
||||
UdpCtrlMessage? toSend = null;
|
||||
bool armedNow;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_pressed.Count > 0)
|
||||
active = ChooseActiveKey(_pressed);
|
||||
armedNow = _armed;
|
||||
if (_armed && _currentMsg != null)
|
||||
toSend = _currentMsg;
|
||||
}
|
||||
|
||||
if (active.HasValue)
|
||||
if (armedNow && toSend != null)
|
||||
{
|
||||
// reenviar leve enquanto segurando
|
||||
await SendKeyOnceAsync(active.Value);
|
||||
try
|
||||
{
|
||||
OperationControl.Models.Variaveis.MostrarLog("Enviando comando UDP Loop");
|
||||
await SendOnceAsync(toSend);
|
||||
_lastSendOkUtc = DateTime.UtcNow;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Se o Send der exception, não explode a task.
|
||||
// Vamos deixar o deadman lidar.
|
||||
}
|
||||
|
||||
// Deadman por falha: se ficou muito tempo sem envio OK, desarma e manda STOP
|
||||
if (SendFailDeadmanMs > 0)
|
||||
{
|
||||
var ms = (DateTime.UtcNow - _lastSendOkUtc).TotalMilliseconds;
|
||||
if (_lastSendOkUtc != DateTime.MinValue && ms > SendFailDeadmanMs)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_armed = false;
|
||||
_currentMsg = null;
|
||||
}
|
||||
await SendStopBurstIfNeededAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try { await Task.Delay(RepeatIntervalMs, ct); }
|
||||
|
|
@ -79,36 +127,46 @@ public sealed class ManualControlSender : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsManualKey(Keys k) =>
|
||||
k == Keys.Up || k == Keys.Down || k == Keys.Left || k == Keys.Right ||
|
||||
k == Keys.Escape;
|
||||
private Task SendBurstAsync(UdpCtrlMessage msg, int count, int intervalMs)
|
||||
=> _udp.SendBurstAsync(UdpReliableChannel.TYPE_COMMAND, msg.ToBytes(), count: count, intervalMs: intervalMs, requestAck: false);
|
||||
|
||||
// Prioridade: se tiver Up/Down e Left/Right junto, você pode escolher uma regra
|
||||
private static Keys ChooseActiveKey(IEnumerable<Keys> keys)
|
||||
private Task SendOnceAsync(UdpCtrlMessage msg)
|
||||
=> _udp.SendBurstAsync(UdpReliableChannel.TYPE_COMMAND, msg.ToBytes(), count: 1, intervalMs: 25, requestAck: false);
|
||||
|
||||
private async Task SendStopBurstIfNeededAsync()
|
||||
{
|
||||
// Exemplo: prioriza ESC, depois Up/Down, depois Left/Right
|
||||
if (keys.Contains(Keys.Escape)) return Keys.Escape;
|
||||
if (keys.Contains(Keys.Up)) return Keys.Up;
|
||||
if (keys.Contains(Keys.Down)) return Keys.Down;
|
||||
if (keys.Contains(Keys.Left)) return Keys.Left;
|
||||
if (keys.Contains(Keys.Right)) return Keys.Right;
|
||||
return Keys.Escape;
|
||||
var now = DateTime.UtcNow;
|
||||
if ((now - _lastStopSentUtc).TotalMilliseconds < MinStopIntervalMs)
|
||||
return;
|
||||
|
||||
_lastStopSentUtc = now;
|
||||
|
||||
var stop = BuildStopMsg();
|
||||
try { await SendBurstAsync(stop, BurstStopCount, BurstStopIntervalMs); }
|
||||
catch { /* não derruba */ }
|
||||
}
|
||||
|
||||
private Task SendKeyBurstAsync(Keys key, int count, int intervalMs)
|
||||
{
|
||||
var payload = UdpKeyPayload.Build(_device, key);
|
||||
return _udp.SendBurstAsync(TYPE_KEY_CMD, payload, count, intervalMs, requestAck: false);
|
||||
}
|
||||
private static bool IsNeutral(UdpCtrlMessage msg)
|
||||
=> msg.Key == AgroBase.Models.Enums.BotoesJoystick.Vazio;
|
||||
|
||||
private Task SendKeyOnceAsync(Keys key)
|
||||
private UdpCtrlMessage BuildStopMsg()
|
||||
{
|
||||
var payload = UdpKeyPayload.Build(_device, key);
|
||||
return _udp.SendAsync(TYPE_KEY_CMD, payload, requestAck: false);
|
||||
return new UdpCtrlMessage
|
||||
{
|
||||
Device = _device,
|
||||
Flags = UdpCtrlFlags.HasKey | UdpCtrlFlags.HasPayload,
|
||||
Key = AgroBase.Models.Enums.BotoesJoystick.Vazio,
|
||||
P1 = 0,
|
||||
P2 = 0
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { _cts?.Cancel(); } catch { }
|
||||
try { _loop?.Wait(200); } catch { }
|
||||
try { _cts?.Dispose(); } catch { }
|
||||
_cts = null;
|
||||
_loop = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1020,6 +1020,7 @@ namespace OperationControl.Windows
|
|||
double perda = conexao?.detalhes?["loss_pct"]?.Value<double>() ?? 0.0;
|
||||
double jitter = conexao?.detalhes?["jitter"]?.Value<double>() ?? 0.0;
|
||||
double bwTotal = conexao?.detalhes?["bw_total_mbps"]?.Value<double>() ?? 0;
|
||||
double bwPct = conexao?.detalhes?["bw_util_pct"]?.Value<double>() ?? 0;
|
||||
double errRate = conexao?.detalhes?["err_rate"]?.Value<double>() ?? 0;
|
||||
graficoAcompanhamento.Atualizar(inicial, momento, new List<GraficoModel.GraficoParametrosModel>()
|
||||
{
|
||||
|
|
@ -1059,6 +1060,12 @@ namespace OperationControl.Windows
|
|||
Unidade = "Mbps",
|
||||
Valor = bwTotal
|
||||
},
|
||||
new GraficoModel.GraficoParametrosModel()
|
||||
{
|
||||
Label = "Banda",
|
||||
Unidade = "%",
|
||||
Valor = bwPct
|
||||
},
|
||||
});
|
||||
break;
|
||||
case AgroBase.Models.Enums.T_Code.Gps:
|
||||
|
|
|
|||
Loading…
Reference in New Issue