using AgroBase; using AgroBase.Models; using System; using System.Threading; using System.Threading.Tasks; public sealed class ManualControlSender : IDisposable { private readonly UdpReliableChannel _udp; 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 BurstStartCount { get; set; } = 6; public int BurstStartIntervalMs { get; set; } = 25; public int BurstStopCount { get; set; } = 10; public int BurstStopIntervalMs { get; set; } = 25; // 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 ?? throw new ArgumentNullException(nameof(udp)); _device = device; } public void Start() { if (_cts != null) return; _cts = new CancellationTokenSource(); _loop = Task.Run(() => Loop(_cts.Token)); } /// /// Chame apenas quando o comando mudou (apertou / soltou / trocou direção). /// O loop cuida do reenvio contínuo. /// public void Update(UdpCtrlMessage msg) { if (msg == null) return; msg.Device = _device; bool isNeutral = IsNeutral(msg); lock (_lock) { if (isNeutral) { _armed = false; _currentMsg = null; } else { _armed = true; _currentMsg = msg; } } // fora do lock if (isNeutral) _ = SendStopBurstIfNeededAsync(msg); else _ = SendBurstAsync(msg, BurstStartCount, BurstStartIntervalMs); } private async Task Loop(CancellationToken ct) { while (!ct.IsCancellationRequested) { UdpCtrlMessage? toSend = null; bool armedNow; lock (_lock) { armedNow = _armed; if (_armed && _currentMsg != null) toSend = _currentMsg; } if (armedNow && toSend != null) { 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) { await SendStopBurstIfNeededAsync(_currentMsg); lock (_lock) { _armed = false; _currentMsg = null; } } } } try { await Task.Delay(RepeatIntervalMs, ct); } catch (TaskCanceledException) { } } } private Task SendBurstAsync(UdpCtrlMessage msg, int count, int intervalMs) => _udp.SendBurstAsync(UdpReliableChannel.TYPE_COMMAND, msg.ToBytes(), count: count, intervalMs: intervalMs, requestAck: false); private Task SendOnceAsync(UdpCtrlMessage msg) => _udp.SendBurstAsync(UdpReliableChannel.TYPE_COMMAND, msg.ToBytes(), count: 1, intervalMs: 25, requestAck: false); private async Task SendStopBurstIfNeededAsync(UdpCtrlMessage msg) { var now = DateTime.UtcNow; if ((now - _lastStopSentUtc).TotalMilliseconds < MinStopIntervalMs) return; _lastStopSentUtc = now; var stop = BuildStopMsg(msg.Key); try { await SendBurstAsync(stop, BurstStopCount, BurstStopIntervalMs); } catch { /* não derruba */ } } private static bool IsNeutral(UdpCtrlMessage msg) => msg.Key == AgroBase.Models.Enums.BotoesJoystick.Vazio || msg.released; private UdpCtrlMessage BuildStopMsg(AgroBase.Models.Enums.BotoesJoystick botao) { return new UdpCtrlMessage { Device = _device, Flags = UdpCtrlFlags.HasKey | UdpCtrlFlags.HasPayload, Key = botao, released = true, P1 = 0, P2 = 0 }; } public void Dispose() { try { _cts?.Cancel(); } catch { } try { _loop?.Wait(200); } catch { } try { _cts?.Dispose(); } catch { } _cts = null; _loop = null; } }