agrobot_base/AgroBase/OperationControl/Services/ManualControlSender.cs

174 lines
5.3 KiB
C#
Raw Normal View History

2026-03-05 13:50:01 +00:00
using AgroBase;
using AgroBase.Models;
using System;
2026-03-05 02:03:31 +00:00
using System.Threading;
using System.Threading.Tasks;
public sealed class ManualControlSender : IDisposable
{
private readonly UdpReliableChannel _udp;
2026-03-05 13:50:01 +00:00
private readonly byte _device;
2026-03-05 02:03:31 +00:00
private readonly object _lock = new();
private CancellationTokenSource? _cts;
private Task? _loop;
2026-03-05 13:50:01 +00:00
private bool _armed;
private UdpCtrlMessage? _currentMsg;
2026-03-05 02:03:31 +00:00
// Ajuste fino
public int RepeatIntervalMs { get; set; } = 250; // 4 Hz
2026-03-05 13:50:01 +00:00
public int BurstStartCount { get; set; } = 6;
public int BurstStartIntervalMs { get; set; } = 25;
2026-03-05 02:03:31 +00:00
public int BurstStopCount { get; set; } = 10;
public int BurstStopIntervalMs { get; set; } = 25;
2026-03-05 13:50:01 +00:00
// 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;
2026-03-05 02:03:31 +00:00
public ManualControlSender(UdpReliableChannel udp, byte device)
{
2026-03-05 13:50:01 +00:00
_udp = udp ?? throw new ArgumentNullException(nameof(udp));
2026-03-05 02:03:31 +00:00
_device = device;
}
public void Start()
{
2026-03-05 13:50:01 +00:00
if (_cts != null) return;
2026-03-05 02:03:31 +00:00
_cts = new CancellationTokenSource();
_loop = Task.Run(() => Loop(_cts.Token));
}
2026-03-05 13:50:01 +00:00
/// <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)
2026-03-05 02:03:31 +00:00
{
2026-03-05 13:50:01 +00:00
if (msg == null) return;
2026-03-05 02:03:31 +00:00
2026-03-05 13:50:01 +00:00
msg.Device = _device;
2026-03-05 02:03:31 +00:00
2026-03-05 13:50:01 +00:00
bool isNeutral = IsNeutral(msg);
2026-03-05 02:03:31 +00:00
2026-03-05 13:50:01 +00:00
lock (_lock)
{
if (isNeutral)
{
_armed = false;
_currentMsg = null;
}
else
{
_armed = true;
_currentMsg = msg;
}
}
2026-03-05 02:03:31 +00:00
2026-03-05 13:50:01 +00:00
// fora do lock
if (isNeutral)
_ = SendStopBurstIfNeededAsync(msg);
2026-03-05 13:50:01 +00:00
else
_ = SendBurstAsync(msg, BurstStartCount, BurstStartIntervalMs);
2026-03-05 02:03:31 +00:00
}
private async Task Loop(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
2026-03-05 13:50:01 +00:00
UdpCtrlMessage? toSend = null;
bool armedNow;
2026-03-05 02:03:31 +00:00
lock (_lock)
{
2026-03-05 13:50:01 +00:00
armedNow = _armed;
if (_armed && _currentMsg != null)
toSend = _currentMsg;
2026-03-05 02:03:31 +00:00
}
2026-03-05 13:50:01 +00:00
if (armedNow && toSend != null)
2026-03-05 02:03:31 +00:00
{
2026-03-05 13:50:01 +00:00
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);
2026-03-05 13:50:01 +00:00
lock (_lock)
{
_armed = false;
_currentMsg = null;
}
}
}
2026-03-05 02:03:31 +00:00
}
try { await Task.Delay(RepeatIntervalMs, ct); }
catch (TaskCanceledException) { }
}
}
2026-03-05 13:50:01 +00:00
private Task SendBurstAsync(UdpCtrlMessage msg, int count, int intervalMs)
=> _udp.SendBurstAsync(UdpReliableChannel.TYPE_COMMAND, msg.ToBytes(), count: count, intervalMs: intervalMs, requestAck: false);
2026-03-05 02:03:31 +00:00
2026-03-05 13:50:01 +00:00
private Task SendOnceAsync(UdpCtrlMessage msg)
=> _udp.SendBurstAsync(UdpReliableChannel.TYPE_COMMAND, msg.ToBytes(), count: 1, intervalMs: 25, requestAck: false);
2026-03-05 02:03:31 +00:00
private async Task SendStopBurstIfNeededAsync(UdpCtrlMessage msg)
2026-03-05 02:03:31 +00:00
{
2026-03-05 13:50:01 +00:00
var now = DateTime.UtcNow;
if ((now - _lastStopSentUtc).TotalMilliseconds < MinStopIntervalMs)
return;
_lastStopSentUtc = now;
var stop = BuildStopMsg(msg.Key);
2026-03-05 13:50:01 +00:00
try { await SendBurstAsync(stop, BurstStopCount, BurstStopIntervalMs); }
catch { /* não derruba */ }
2026-03-05 02:03:31 +00:00
}
2026-03-05 13:50:01 +00:00
private static bool IsNeutral(UdpCtrlMessage msg)
=> msg.Key == AgroBase.Models.Enums.BotoesJoystick.Vazio || msg.released;
2026-03-05 13:50:01 +00:00
private UdpCtrlMessage BuildStopMsg(AgroBase.Models.Enums.BotoesJoystick botao)
2026-03-05 02:03:31 +00:00
{
2026-03-05 13:50:01 +00:00
return new UdpCtrlMessage
{
Device = _device,
Flags = UdpCtrlFlags.HasKey | UdpCtrlFlags.HasPayload,
Key = botao,
released = true,
2026-03-05 13:50:01 +00:00
P1 = 0,
P2 = 0
};
2026-03-05 02:03:31 +00:00
}
public void Dispose()
{
try { _cts?.Cancel(); } catch { }
2026-03-05 13:50:01 +00:00
try { _loop?.Wait(200); } catch { }
try { _cts?.Dispose(); } catch { }
_cts = null;
_loop = null;
2026-03-05 02:03:31 +00:00
}
2026-03-05 02:03:31 +00:00
}