172 lines
5.2 KiB
C#
172 lines
5.2 KiB
C#
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));
|
|
}
|
|
|
|
/// <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 (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();
|
|
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)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_armed = false;
|
|
_currentMsg = null;
|
|
}
|
|
await SendStopBurstIfNeededAsync();
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
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 static bool IsNeutral(UdpCtrlMessage msg)
|
|
=> msg.Key == AgroBase.Models.Enums.BotoesJoystick.Vazio;
|
|
|
|
private UdpCtrlMessage BuildStopMsg()
|
|
{
|
|
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;
|
|
}
|
|
} |