diff --git a/AgroBase/AgroBase/AgroBase.csproj b/AgroBase/AgroBase/AgroBase.csproj
index 94e27671c..fab4b68ed 100644
--- a/AgroBase/AgroBase/AgroBase.csproj
+++ b/AgroBase/AgroBase/AgroBase.csproj
@@ -517,6 +517,7 @@
frmSimulacaoMapeamentoVisual.cs
+
diff --git a/AgroBase/AgroBase/Forms/frmInstancial.cs b/AgroBase/AgroBase/Forms/frmInstancial.cs
index 80fc81833..d1c968f5e 100644
--- a/AgroBase/AgroBase/Forms/frmInstancial.cs
+++ b/AgroBase/AgroBase/Forms/frmInstancial.cs
@@ -1,4 +1,4 @@
-using AgroBase.Forms.IHM;
+using AgroBase.Forms.IHM;
using AgroBase.Models;
using AgroBase.Services;
using System;
@@ -14,134 +14,484 @@ namespace AgroBase.Forms
public static frmPrincipal frmPrincipal = new frmPrincipal();
public static frmJoystick frmJoystick = new frmJoystick();
- public static frmIHM frmIHM = new frmIHM()
- {
- FormBorderStyle = FormBorderStyle.None,
- //WindowState = FormWindowState.Maximized,
- //TopMost = true
+ public static frmIHM frmIHM = new frmIHM()
+ {
+ FormBorderStyle = FormBorderStyle.None,
+ //WindowState = FormWindowState.Maximized,
+ //TopMost = true
};
+ private static readonly SemaphoreSlim _startupGate =
+ new SemaphoreSlim(1, 1);
+
+ private static readonly SemaphoreSlim _shutdownGate =
+ new SemaphoreSlim(1, 1);
+
+ private static CancellationTokenSource _appCts;
+
+ private static bool _startupConcluido;
+ private static bool _shutdownIniciado;
+
+ private bool _allowClose;
+
public frmInstancial()
{
InitializeComponent();
- ThreadPool.SetMinThreads(workerThreads: 50, completionPortThreads: 50);
+ /*
+ * O construtor precisa ser leve.
+ * Nada de abrir MQTT, Redis, Python, CAN, Livox ou operadores aqui.
+ * O WinForms ainda está criando handles e a aplicação ainda não
+ * possui lifecycle seguro para aguardar falhas.
+ */
+ ThreadPool.SetMinThreads(
+ workerThreads: 50,
+ completionPortThreads: 50
+ );
+ }
- Variaveis.IniciarMQTT();
-
- //Variaveis.IniciarUDP();
-
- APIService.IniciarRotinas();
-
- GeneralJoystick.IniciarRotinas();
-
- VersionamentoService.IniciarRotinas();
-
- PythonService.EncerrarProcessos();
-
- RedisService.Iniciar();
- RedisService.LimparDadosIniciais();
-
- LivoxManagerProcess.Start();
-
- Task.Run(async () =>
+ private async void frmInstancial_Load(object sender, EventArgs e)
+ {
+ try
{
- await VariaveisOperacao.Operadores.IniciarProcessamento(false);
- });
+ await InicializarServicosAsync();
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Load] Falha crítica na inicialização: " +
+ ex
+ );
- AudioAlertaService.SelecionarVoz(VariaveisEquipamento.VozAlerta);
+ try
+ {
+ MessageBox.Show(
+ "Falha ao inicializar o sistema:\n\n" +
+ ex.Message,
+ "AgroBase",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Error
+ );
+ }
+ catch { }
+ }
}
private void frmInstancial_Shown(object sender, EventArgs e)
{
- if (!Variaveis.Producao)
+ try
{
- frmIHM.FormClosing += FrmIHM_FormClosing;
- frmPrincipal.FormClosing += frmPrincipal_FormClosing;
- frmPrincipal.Show();
- }
- else
- {
- frmIHM.FormClosing += frmPrincipal_FormClosing;
- frmIHM.Show();
- }
+ if (!Variaveis.Producao)
+ {
+ frmIHM.FormClosing -= FrmIHM_FormClosing;
+ frmPrincipal.FormClosing -= frmPrincipal_FormClosing;
- this.Hide();
+ frmIHM.FormClosing += FrmIHM_FormClosing;
+ frmPrincipal.FormClosing += frmPrincipal_FormClosing;
+
+ frmPrincipal.Show();
+ }
+ else
+ {
+ frmIHM.FormClosing -= frmPrincipal_FormClosing;
+ frmIHM.FormClosing += frmPrincipal_FormClosing;
+
+ frmIHM.Show();
+ }
+
+ Hide();
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Shown] " + ex
+ );
+ }
}
-
-
- private void frmInstancial_Load(object sender, EventArgs e)
+ private static async Task InicializarServicosAsync()
{
- FuncoesGlobais.AddApplicationToStartup();
- IniciarConexaoMqtt();
+ await _startupGate.WaitAsync()
+ .ConfigureAwait(false);
- Control control = new Control();
- if (!Variaveis.Producao)
+ try
{
- control = frmPrincipal;
- }
- else
- {
- control = frmIHM;
- }
+ if (_startupConcluido)
+ return;
+
+ _shutdownIniciado = false;
+ Variaveis.Fechando = false;
+
+ _appCts?.Dispose();
+ _appCts = new CancellationTokenSource();
+
+ FuncoesGlobais.AddApplicationToStartup();
+
+ /*
+ * Ordem pensada para campo:
+ * 1. limpar processos externos velhos;
+ * 2. subir infraestrutura local;
+ * 3. subir comunicação MQTT/UDP;
+ * 4. iniciar rotinas e operadores;
+ * 5. iniciar varredura física.
+ */
+
+ PythonService.EncerrarProcessos();
+
+ RedisService.Iniciar();
+ RedisService.LimparDadosIniciais();
+
+ await Variaveis.IniciarMqttAsync(_appCts.Token)
+ .ConfigureAwait(false);
+
+ /*
+ * Hoje pode ficar comentado se UDP não for usado no rover,
+ * mas o método fica aqui no lugar certo do lifecycle.
+ */
+ //Variaveis.IniciarUDP();
+
+ APIService.IniciarRotinas();
+ GeneralJoystick.IniciarRotinas();
+ VersionamentoService.IniciarRotinas();
+
+ LivoxManagerProcess.Start();
+
+ await VariaveisOperacao
+ .Operadores
+ .IniciarProcessamento(false)
+ .ConfigureAwait(false);
+
+ AudioAlertaService.SelecionarVoz(
+ VariaveisEquipamento.VozAlerta
+ );
+
+ IniciarTimerVarreduraDispositivos();
+
+ _startupConcluido = true;
+
+ Variaveis.MostrarLog(
+ "[frmInstancial] Inicialização concluída."
+ );
+ }
+ catch
+ {
+ await EncerrarProcessosInternoAsync(
+ sairProcesso: false
+ ).ConfigureAwait(false);
+
+ throw;
+ }
+ finally
+ {
+ _startupGate.Release();
+ }
+ }
+
+ private static void IniciarTimerVarreduraDispositivos()
+ {
+ if (tmrVarreduraDispositivos != null)
+ return;
+
+ tmrVarreduraDispositivos =
+ new AsyncTaskTimerModel(
+ "tmrVarreduraDispositivos",
+ tmrVarreduraDispositivos_Tick,
+ SerialService.InteraloVerificacao,
+ null,
+ 15000
+ );
- tmrVarreduraDispositivos = new AsyncTaskTimerModel("tmrVarreduraDispositivos", tmrVarreduraDispositivos_Tick, SerialService.InteraloVerificacao, null, 15000);
tmrVarreduraDispositivos.Start();
}
private static async Task tmrVarreduraDispositivos_Tick()
{
- await SerialService.RealizarVarreduraPortasUSB();
+ if (Variaveis.Fechando)
+ return;
+
+ await SerialService
+ .RealizarVarreduraPortasUSB()
+ .ConfigureAwait(false);
}
+ /*
+ * Método mantido para compatibilidade com chamadas antigas.
+ * Não é mais chamado no Load, porque o startup correto é
+ * Variaveis.IniciarMqttAsync().
+ */
private async void IniciarConexaoMqtt()
{
- await Variaveis.MqttServiceLocal.ConnectAsync();
+ try
+ {
+ await Variaveis.IniciarMqttAsync();
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.IniciarConexaoMqtt] " +
+ ex.Message
+ );
+ }
}
- public static async Task EncerrarProcessos()
+ public static Task EncerrarProcessos()
+ {
+ return EncerrarProcessos(sairProcesso: false);
+ }
+
+ public static async Task EncerrarProcessos(
+ bool sairProcesso)
+ {
+ await _shutdownGate.WaitAsync()
+ .ConfigureAwait(false);
+
+ try
+ {
+ if (_shutdownIniciado)
+ return;
+
+ _shutdownIniciado = true;
+
+ await EncerrarProcessosInternoAsync(
+ sairProcesso
+ ).ConfigureAwait(false);
+ }
+ finally
+ {
+ _shutdownGate.Release();
+ }
+ }
+
+ private static async Task EncerrarProcessosInternoAsync(
+ bool sairProcesso)
{
Variaveis.Fechando = true;
- LivoxManagerProcess.Dispose();
+ try { _appCts?.Cancel(); } catch { }
- PythonService.EncerrarProcessos();
-
- Variaveis.StopUdpChannel();
-
- foreach (var Disp in Variaveis.DispositivosConectados)
+ /*
+ * Primeiro paramos timers para nenhuma rotina reentrar
+ * enquanto desmontamos serial, CAN, MQTT e processos.
+ */
+ try
{
- Disp.EnviarProtocolosConfiguracao(false);
- Disp.SalvarParametros(Disp.Dados);
+ await AsyncTaskTimerModel
+ .PararTimersAsync()
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao parar timers: " +
+ ex.Message
+ );
}
- var timeout = DateTime.Now.AddSeconds(10);
- while (!CanManager.CanService.FilaLiberada && DateTime.Now < timeout)
+ tmrVarreduraDispositivos = null;
+
+ try
{
- await Task.Delay(200);
+ await SerialService
+ .EncerrarAsync()
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao encerrar SerialService: " +
+ ex.Message
+ );
}
- AsyncTaskTimerModel.PararTimers();
+ /*
+ * Tenta avisar dispositivos antes de fechar CAN/MQTT/processos.
+ */
+ try
+ {
+ foreach (var disp in Variaveis.DispositivosConectados)
+ {
+ try
+ {
+ disp.EnviarProtocolosConfiguracao(false);
+ disp.SalvarParametros(disp.Dados);
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao salvar " +
+ disp.Dispositivo + ": " + ex.Message
+ );
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao percorrer dispositivos: " +
+ ex.Message
+ );
+ }
- try { Environment.Exit(0); } catch { }
+ /*
+ * Dá uma janela curta para a fila CAN drenar.
+ * Usa DateTime aqui só por compatibilidade com o padrão atual,
+ * mas sem travar indefinidamente.
+ */
+ try
+ {
+ var timeout = DateTime.Now.AddSeconds(10);
+
+ while (!CanManager.CanService.FilaLiberada &&
+ DateTime.Now < timeout)
+ {
+ await Task.Delay(200)
+ .ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro aguardando fila CAN: " +
+ ex.Message
+ );
+ }
+
+ try
+ {
+ LivoxManagerProcess.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao encerrar Livox: " +
+ ex.Message
+ );
+ }
+
+ try
+ {
+ PythonService.EncerrarProcessos();
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao encerrar Python: " +
+ ex.Message
+ );
+ }
+
+ try
+ {
+ Variaveis.StopUdpChannel();
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao encerrar UDP: " +
+ ex.Message
+ );
+ }
+
+ try
+ {
+ await GPSService
+ .EncerrarAsync()
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao encerrar GPS: " +
+ ex.Message
+ );
+ }
+
+ try
+ {
+ await Variaveis
+ .EncerrarMqttAsync()
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.Encerrar] Erro ao encerrar MQTT: " +
+ ex.Message
+ );
+ }
+
+ try
+ {
+ _appCts?.Dispose();
+ _appCts = null;
+ }
+ catch { }
+
+ _startupConcluido = false;
+
+ if (sairProcesso)
+ {
+ try
+ {
+ Environment.Exit(0);
+ }
+ catch { }
+ }
}
-
-
-
-
- private async void frmPrincipal_FormClosing(object sender, FormClosingEventArgs e)
+ private async void frmPrincipal_FormClosing(
+ object sender,
+ FormClosingEventArgs e)
{
- await EncerrarProcessos();
+ if (_allowClose)
+ return;
+
e.Cancel = true;
+
+ try
+ {
+ await EncerrarProcessos(
+ sairProcesso: false
+ );
+
+ _allowClose = true;
+
+ BeginInvoke(
+ new Action(() =>
+ {
+ try
+ {
+ Close();
+ }
+ catch { }
+ })
+ );
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[frmInstancial.FormClosing] " + ex
+ );
+
+ try
+ {
+ Environment.Exit(1);
+ }
+ catch { }
+ }
}
- private void FrmIHM_FormClosing(object sender, FormClosingEventArgs e)
+ private void FrmIHM_FormClosing(
+ object sender,
+ FormClosingEventArgs e)
{
+ /*
+ * Em modo desenvolvimento, fechar a IHM apenas oculta a janela.
+ * Em produção, o evento de fechamento da IHM é associado ao
+ * frmPrincipal_FormClosing e faz shutdown real.
+ */
e.Cancel = true;
frmIHM.Hide();
}
-
}
}
diff --git a/AgroBase/AgroBase/Models/BaseLinkState.cs b/AgroBase/AgroBase/Models/BaseLinkState.cs
new file mode 100644
index 000000000..b920f4d21
--- /dev/null
+++ b/AgroBase/AgroBase/Models/BaseLinkState.cs
@@ -0,0 +1,636 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+
+namespace AgroBase.Models
+{
+ ///
+ /// Mantém o estado da comunicação entre o rover e a base.
+ ///
+ /// Regras importantes:
+ /// - Não armazena estado de enlace dentro de GPSModel.
+ /// - Mede idades com Stopwatch, portanto não sofre com ajuste do relógio do Windows.
+ /// - É thread-safe para callbacks MQTT, timers e UI.
+ /// - Uma nova sessão invalida confirmações antigas de descoberta.
+ ///
+ public sealed class BaseLinkState
+ {
+ private readonly object _sync = new object();
+
+ private int _brokerConnected;
+ private int _discoveryAcknowledged;
+
+ private long _generation;
+
+ private long _lastBrokerConnectedMono;
+ private long _lastBrokerDisconnectedMono;
+ private long _lastDiscoverySentMono;
+ private long _lastDiscoveryAckMono;
+ private long _lastHeartbeatMono;
+ private long _lastRtcmMono;
+ private long _lastPositionMono;
+ private long _lastCommandMono;
+ private long _lastAnyMessageMono;
+
+ private long _lastBrokerConnectedUtcTicks;
+ private long _lastBrokerDisconnectedUtcTicks;
+ private long _lastDiscoverySentUtcTicks;
+ private long _lastDiscoveryAckUtcTicks;
+ private long _lastHeartbeatUtcTicks;
+ private long _lastRtcmUtcTicks;
+ private long _lastPositionUtcTicks;
+ private long _lastCommandUtcTicks;
+ private long _lastAnyMessageUtcTicks;
+
+ private long _brokerConnectCount;
+ private long _brokerDisconnectCount;
+ private long _discoverySentCount;
+ private long _discoveryAckCount;
+ private long _heartbeatCount;
+ private long _rtcmCount;
+ private long _positionCount;
+ private long _commandCount;
+ private long _anyMessageCount;
+ private long _invalidDiscoveryAckCount;
+
+ private long _discoverySequence;
+
+ private string _sessionId;
+ private string _baseId;
+ private string _roverId;
+ private string _lastDisconnectReason;
+ private string _lastError;
+
+ private int _heartbeatTimeoutMs;
+ private int _criticalChannelTimeoutMs;
+ private int _rtcmTimeoutMs;
+ private int _discoveryRetryMs;
+
+ public BaseLinkState(
+ int heartbeatTimeoutMs = 10000,
+ int criticalChannelTimeoutMs = 12000,
+ int rtcmTimeoutMs = 5000,
+ int discoveryRetryMs = 3000)
+ {
+ SetTimeouts(
+ heartbeatTimeoutMs,
+ criticalChannelTimeoutMs,
+ rtcmTimeoutMs,
+ discoveryRetryMs
+ );
+
+ BeginNewSession();
+ }
+
+ public bool BrokerConnected
+ {
+ get { return Volatile.Read(ref _brokerConnected) == 1; }
+ }
+
+ public bool DiscoveryAcknowledged
+ {
+ get { return Volatile.Read(ref _discoveryAcknowledged) == 1; }
+ }
+
+ ///
+ /// A base é considerada conhecida quando o broker está conectado e:
+ /// - a descoberta da sessão atual foi confirmada; ou
+ /// - um heartbeat recente comprova comunicação direta com a base.
+ ///
+ /// O fallback por heartbeat mantém compatibilidade até o protocolo
+ /// explícito de discovery/ack ser implantado.
+ ///
+ public bool BaseKnown
+ {
+ get
+ {
+ return BrokerConnected &&
+ (DiscoveryAcknowledged || HeartbeatHealthy);
+ }
+ }
+
+ public bool HeartbeatHealthy
+ {
+ get
+ {
+ return GetAgeMs(Interlocked.Read(ref _lastHeartbeatMono))
+ <= Volatile.Read(ref _heartbeatTimeoutMs);
+ }
+ }
+
+ public bool RtcmRecent
+ {
+ get
+ {
+ return GetAgeMs(Interlocked.Read(ref _lastRtcmMono))
+ <= Volatile.Read(ref _rtcmTimeoutMs);
+ }
+ }
+
+ ///
+ /// Indica que o canal crítico está comprovadamente vivo.
+ /// RTCM possui uma propriedade separada porque pode não existir
+ /// durante configuração, simulação ou ausência temporária de correção.
+ ///
+ public bool CriticalChannelHealthy
+ {
+ get
+ {
+ if (!BrokerConnected)
+ return false;
+
+ double lastCriticalAge = Math.Min(
+ LastHeartbeatAgeMs,
+ Math.Min(LastRtcmAgeMs, LastCommandAgeMs)
+ );
+
+ return lastCriticalAge <= Volatile.Read(ref _criticalChannelTimeoutMs);
+ }
+ }
+
+ ///
+ /// A descoberta só deve ser publicada quando o broker está conectado,
+ /// a base ainda não foi reconhecida e o intervalo de retry venceu.
+ ///
+ public bool DiscoveryNeeded
+ {
+ get
+ {
+ if (!BrokerConnected || BaseKnown)
+ return false;
+
+ return LastDiscoverySentAgeMs >= Volatile.Read(ref _discoveryRetryMs);
+ }
+ }
+
+ public long Generation
+ {
+ get { return Interlocked.Read(ref _generation); }
+ }
+
+ public string SessionId
+ {
+ get
+ {
+ lock (_sync)
+ return _sessionId;
+ }
+ }
+
+ public string BaseId
+ {
+ get
+ {
+ lock (_sync)
+ return _baseId;
+ }
+ }
+
+ public string RoverId
+ {
+ get
+ {
+ lock (_sync)
+ return _roverId;
+ }
+ }
+
+ public double LastHeartbeatAgeMs
+ {
+ get { return GetAgeMs(Interlocked.Read(ref _lastHeartbeatMono)); }
+ }
+
+ public double LastRtcmAgeMs
+ {
+ get { return GetAgeMs(Interlocked.Read(ref _lastRtcmMono)); }
+ }
+
+ public double LastPositionAgeMs
+ {
+ get { return GetAgeMs(Interlocked.Read(ref _lastPositionMono)); }
+ }
+
+ public double LastCommandAgeMs
+ {
+ get { return GetAgeMs(Interlocked.Read(ref _lastCommandMono)); }
+ }
+
+ public double LastAnyMessageAgeMs
+ {
+ get { return GetAgeMs(Interlocked.Read(ref _lastAnyMessageMono)); }
+ }
+
+ public double LastDiscoverySentAgeMs
+ {
+ get { return GetAgeMs(Interlocked.Read(ref _lastDiscoverySentMono)); }
+ }
+
+ public double LastDiscoveryAckAgeMs
+ {
+ get { return GetAgeMs(Interlocked.Read(ref _lastDiscoveryAckMono)); }
+ }
+
+ public void SetTimeouts(
+ int heartbeatTimeoutMs,
+ int criticalChannelTimeoutMs,
+ int rtcmTimeoutMs,
+ int discoveryRetryMs)
+ {
+ if (heartbeatTimeoutMs <= 0)
+ throw new ArgumentOutOfRangeException(nameof(heartbeatTimeoutMs));
+
+ if (criticalChannelTimeoutMs <= 0)
+ throw new ArgumentOutOfRangeException(nameof(criticalChannelTimeoutMs));
+
+ if (rtcmTimeoutMs <= 0)
+ throw new ArgumentOutOfRangeException(nameof(rtcmTimeoutMs));
+
+ if (discoveryRetryMs <= 0)
+ throw new ArgumentOutOfRangeException(nameof(discoveryRetryMs));
+
+ Volatile.Write(ref _heartbeatTimeoutMs, heartbeatTimeoutMs);
+ Volatile.Write(ref _criticalChannelTimeoutMs, criticalChannelTimeoutMs);
+ Volatile.Write(ref _rtcmTimeoutMs, rtcmTimeoutMs);
+ Volatile.Write(ref _discoveryRetryMs, discoveryRetryMs);
+ }
+
+ ///
+ /// Inicia uma nova sessão lógica do rover.
+ /// Toda confirmação de descoberta anterior é invalidada.
+ ///
+ public string BeginNewSession(string roverId = null, string sessionId = null)
+ {
+ string newSessionId = string.IsNullOrWhiteSpace(sessionId)
+ ? Guid.NewGuid().ToString("N")
+ : sessionId.Trim();
+
+ lock (_sync)
+ {
+ _sessionId = newSessionId;
+ _roverId = roverId;
+ _baseId = null;
+ _lastDisconnectReason = null;
+ _lastError = null;
+ }
+
+ Interlocked.Increment(ref _generation);
+ Interlocked.Exchange(ref _discoveryAcknowledged, 0);
+ Interlocked.Exchange(ref _discoverySequence, 0);
+
+ ResetEventTimestamps();
+
+ return newSessionId;
+ }
+
+ public void MarkBrokerConnected()
+ {
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+
+ Interlocked.Exchange(ref _brokerConnected, 1);
+ Interlocked.Exchange(ref _lastBrokerConnectedMono, nowMono);
+ Interlocked.Exchange(ref _lastBrokerConnectedUtcTicks, nowUtc);
+ Interlocked.Increment(ref _brokerConnectCount);
+
+ lock (_sync)
+ {
+ _lastDisconnectReason = null;
+ _lastError = null;
+ }
+ }
+
+ public void MarkBrokerDisconnected(string reason = null)
+ {
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+
+ Interlocked.Exchange(ref _brokerConnected, 0);
+ Interlocked.Exchange(ref _discoveryAcknowledged, 0);
+ Interlocked.Exchange(ref _lastBrokerDisconnectedMono, nowMono);
+ Interlocked.Exchange(ref _lastBrokerDisconnectedUtcTicks, nowUtc);
+ Interlocked.Increment(ref _brokerDisconnectCount);
+
+ lock (_sync)
+ {
+ _baseId = null;
+ _lastDisconnectReason = reason;
+ }
+ }
+
+ ///
+ /// Registra envio de discovery e retorna a sequência utilizada.
+ ///
+ public long MarkDiscoverySent()
+ {
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+ long sequence = Interlocked.Increment(ref _discoverySequence);
+
+ Interlocked.Exchange(ref _lastDiscoverySentMono, nowMono);
+ Interlocked.Exchange(ref _lastDiscoverySentUtcTicks, nowUtc);
+ Interlocked.Increment(ref _discoverySentCount);
+
+ return sequence;
+ }
+
+ ///
+ /// Confirma a descoberta somente se o sessionId recebido pertencer
+ /// à sessão atual. Retorna false para ACK antigo ou inválido.
+ ///
+ public bool TryMarkDiscoveryAcknowledged(
+ string sessionId,
+ string baseId = null)
+ {
+ string currentSession;
+
+ lock (_sync)
+ currentSession = _sessionId;
+
+ if (string.IsNullOrWhiteSpace(sessionId) ||
+ !string.Equals(
+ currentSession,
+ sessionId.Trim(),
+ StringComparison.Ordinal))
+ {
+ Interlocked.Increment(ref _invalidDiscoveryAckCount);
+ return false;
+ }
+
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+
+ lock (_sync)
+ {
+ _baseId = baseId;
+ _lastError = null;
+ }
+
+ Interlocked.Exchange(ref _discoveryAcknowledged, 1);
+ Interlocked.Exchange(ref _lastDiscoveryAckMono, nowMono);
+ Interlocked.Exchange(ref _lastDiscoveryAckUtcTicks, nowUtc);
+ Interlocked.Increment(ref _discoveryAckCount);
+
+ MarkAnyMessageInternal(nowMono, nowUtc);
+ return true;
+ }
+
+ ///
+ /// Compatibilidade temporária enquanto a base ainda não publica
+ /// discovery_ack com sessionId. Um heartbeat recente já comprova
+ /// que existe comunicação direta com a base.
+ ///
+ public void MarkLegacyBaseDetected(string baseId = null)
+ {
+ lock (_sync)
+ {
+ if (!string.IsNullOrWhiteSpace(baseId))
+ _baseId = baseId;
+ }
+
+ MarkHeartbeat();
+ }
+
+ public void MarkHeartbeat()
+ {
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+
+ Interlocked.Exchange(ref _lastHeartbeatMono, nowMono);
+ Interlocked.Exchange(ref _lastHeartbeatUtcTicks, nowUtc);
+ Interlocked.Increment(ref _heartbeatCount);
+
+ MarkAnyMessageInternal(nowMono, nowUtc);
+ }
+
+ public void MarkRtcm()
+ {
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+
+ Interlocked.Exchange(ref _lastRtcmMono, nowMono);
+ Interlocked.Exchange(ref _lastRtcmUtcTicks, nowUtc);
+ Interlocked.Increment(ref _rtcmCount);
+
+ MarkAnyMessageInternal(nowMono, nowUtc);
+ }
+
+ public void MarkPosition()
+ {
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+
+ Interlocked.Exchange(ref _lastPositionMono, nowMono);
+ Interlocked.Exchange(ref _lastPositionUtcTicks, nowUtc);
+ Interlocked.Increment(ref _positionCount);
+
+ MarkAnyMessageInternal(nowMono, nowUtc);
+ }
+
+ public void MarkCommand()
+ {
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+
+ Interlocked.Exchange(ref _lastCommandMono, nowMono);
+ Interlocked.Exchange(ref _lastCommandUtcTicks, nowUtc);
+ Interlocked.Increment(ref _commandCount);
+
+ MarkAnyMessageInternal(nowMono, nowUtc);
+ }
+
+ public void MarkAnyMessage()
+ {
+ long nowMono = Stopwatch.GetTimestamp();
+ long nowUtc = DateTime.UtcNow.Ticks;
+ MarkAnyMessageInternal(nowMono, nowUtc);
+ }
+
+ public void RecordError(string error)
+ {
+ lock (_sync)
+ _lastError = error;
+ }
+
+ public void ClearError()
+ {
+ lock (_sync)
+ _lastError = null;
+ }
+
+ public BaseLinkSnapshot GetSnapshot()
+ {
+ string sessionId;
+ string baseId;
+ string roverId;
+ string disconnectReason;
+ string lastError;
+
+ lock (_sync)
+ {
+ sessionId = _sessionId;
+ baseId = _baseId;
+ roverId = _roverId;
+ disconnectReason = _lastDisconnectReason;
+ lastError = _lastError;
+ }
+
+ return new BaseLinkSnapshot
+ {
+ BrokerConnected = BrokerConnected,
+ DiscoveryAcknowledged = DiscoveryAcknowledged,
+ BaseKnown = BaseKnown,
+ HeartbeatHealthy = HeartbeatHealthy,
+ RtcmRecent = RtcmRecent,
+ CriticalChannelHealthy = CriticalChannelHealthy,
+ DiscoveryNeeded = DiscoveryNeeded,
+
+ Generation = Generation,
+ SessionId = sessionId,
+ BaseId = baseId,
+ RoverId = roverId,
+
+ HeartbeatTimeoutMs = Volatile.Read(ref _heartbeatTimeoutMs),
+ CriticalChannelTimeoutMs = Volatile.Read(ref _criticalChannelTimeoutMs),
+ RtcmTimeoutMs = Volatile.Read(ref _rtcmTimeoutMs),
+ DiscoveryRetryMs = Volatile.Read(ref _discoveryRetryMs),
+
+ LastBrokerConnectedUtc = ReadUtcDateTime(ref _lastBrokerConnectedUtcTicks),
+ LastBrokerDisconnectedUtc = ReadUtcDateTime(ref _lastBrokerDisconnectedUtcTicks),
+ LastDiscoverySentUtc = ReadUtcDateTime(ref _lastDiscoverySentUtcTicks),
+ LastDiscoveryAckUtc = ReadUtcDateTime(ref _lastDiscoveryAckUtcTicks),
+ LastHeartbeatUtc = ReadUtcDateTime(ref _lastHeartbeatUtcTicks),
+ LastRtcmUtc = ReadUtcDateTime(ref _lastRtcmUtcTicks),
+ LastPositionUtc = ReadUtcDateTime(ref _lastPositionUtcTicks),
+ LastCommandUtc = ReadUtcDateTime(ref _lastCommandUtcTicks),
+ LastAnyMessageUtc = ReadUtcDateTime(ref _lastAnyMessageUtcTicks),
+
+ LastDiscoverySentAgeMs = LastDiscoverySentAgeMs,
+ LastDiscoveryAckAgeMs = LastDiscoveryAckAgeMs,
+ LastHeartbeatAgeMs = LastHeartbeatAgeMs,
+ LastRtcmAgeMs = LastRtcmAgeMs,
+ LastPositionAgeMs = LastPositionAgeMs,
+ LastCommandAgeMs = LastCommandAgeMs,
+ LastAnyMessageAgeMs = LastAnyMessageAgeMs,
+
+ BrokerConnectCount = Interlocked.Read(ref _brokerConnectCount),
+ BrokerDisconnectCount = Interlocked.Read(ref _brokerDisconnectCount),
+ DiscoverySentCount = Interlocked.Read(ref _discoverySentCount),
+ DiscoveryAckCount = Interlocked.Read(ref _discoveryAckCount),
+ InvalidDiscoveryAckCount = Interlocked.Read(ref _invalidDiscoveryAckCount),
+ HeartbeatCount = Interlocked.Read(ref _heartbeatCount),
+ RtcmCount = Interlocked.Read(ref _rtcmCount),
+ PositionCount = Interlocked.Read(ref _positionCount),
+ CommandCount = Interlocked.Read(ref _commandCount),
+ AnyMessageCount = Interlocked.Read(ref _anyMessageCount),
+ DiscoverySequence = Interlocked.Read(ref _discoverySequence),
+
+ LastDisconnectReason = disconnectReason,
+ LastError = lastError
+ };
+ }
+
+ private void ResetEventTimestamps()
+ {
+ Interlocked.Exchange(ref _lastDiscoverySentMono, 0);
+ Interlocked.Exchange(ref _lastDiscoveryAckMono, 0);
+ Interlocked.Exchange(ref _lastHeartbeatMono, 0);
+ Interlocked.Exchange(ref _lastRtcmMono, 0);
+ Interlocked.Exchange(ref _lastPositionMono, 0);
+ Interlocked.Exchange(ref _lastCommandMono, 0);
+ Interlocked.Exchange(ref _lastAnyMessageMono, 0);
+
+ Interlocked.Exchange(ref _lastDiscoverySentUtcTicks, 0);
+ Interlocked.Exchange(ref _lastDiscoveryAckUtcTicks, 0);
+ Interlocked.Exchange(ref _lastHeartbeatUtcTicks, 0);
+ Interlocked.Exchange(ref _lastRtcmUtcTicks, 0);
+ Interlocked.Exchange(ref _lastPositionUtcTicks, 0);
+ Interlocked.Exchange(ref _lastCommandUtcTicks, 0);
+ Interlocked.Exchange(ref _lastAnyMessageUtcTicks, 0);
+ }
+
+ private void MarkAnyMessageInternal(long nowMono, long nowUtc)
+ {
+ Interlocked.Exchange(ref _lastAnyMessageMono, nowMono);
+ Interlocked.Exchange(ref _lastAnyMessageUtcTicks, nowUtc);
+ Interlocked.Increment(ref _anyMessageCount);
+ }
+
+ private static double GetAgeMs(long timestamp)
+ {
+ if (timestamp <= 0)
+ return double.PositiveInfinity;
+
+ long now = Stopwatch.GetTimestamp();
+ long delta = now - timestamp;
+
+ if (delta <= 0)
+ return 0.0;
+
+ return delta * 1000.0 / Stopwatch.Frequency;
+ }
+
+ private static DateTime? ReadUtcDateTime(ref long ticksField)
+ {
+ long ticks = Interlocked.Read(ref ticksField);
+
+ if (ticks <= 0)
+ return null;
+
+ return new DateTime(ticks, DateTimeKind.Utc);
+ }
+ }
+
+ public sealed class BaseLinkSnapshot
+ {
+ public bool BrokerConnected { get; set; }
+ public bool DiscoveryAcknowledged { get; set; }
+ public bool BaseKnown { get; set; }
+ public bool HeartbeatHealthy { get; set; }
+ public bool RtcmRecent { get; set; }
+ public bool CriticalChannelHealthy { get; set; }
+ public bool DiscoveryNeeded { get; set; }
+
+ public long Generation { get; set; }
+ public string SessionId { get; set; }
+ public string BaseId { get; set; }
+ public string RoverId { get; set; }
+
+ public int HeartbeatTimeoutMs { get; set; }
+ public int CriticalChannelTimeoutMs { get; set; }
+ public int RtcmTimeoutMs { get; set; }
+ public int DiscoveryRetryMs { get; set; }
+
+ public DateTime? LastBrokerConnectedUtc { get; set; }
+ public DateTime? LastBrokerDisconnectedUtc { get; set; }
+ public DateTime? LastDiscoverySentUtc { get; set; }
+ public DateTime? LastDiscoveryAckUtc { get; set; }
+ public DateTime? LastHeartbeatUtc { get; set; }
+ public DateTime? LastRtcmUtc { get; set; }
+ public DateTime? LastPositionUtc { get; set; }
+ public DateTime? LastCommandUtc { get; set; }
+ public DateTime? LastAnyMessageUtc { get; set; }
+
+ public double LastDiscoverySentAgeMs { get; set; }
+ public double LastDiscoveryAckAgeMs { get; set; }
+ public double LastHeartbeatAgeMs { get; set; }
+ public double LastRtcmAgeMs { get; set; }
+ public double LastPositionAgeMs { get; set; }
+ public double LastCommandAgeMs { get; set; }
+ public double LastAnyMessageAgeMs { get; set; }
+
+ public long BrokerConnectCount { get; set; }
+ public long BrokerDisconnectCount { get; set; }
+ public long DiscoverySentCount { get; set; }
+ public long DiscoveryAckCount { get; set; }
+ public long InvalidDiscoveryAckCount { get; set; }
+ public long HeartbeatCount { get; set; }
+ public long RtcmCount { get; set; }
+ public long PositionCount { get; set; }
+ public long CommandCount { get; set; }
+ public long AnyMessageCount { get; set; }
+ public long DiscoverySequence { get; set; }
+
+ public string LastDisconnectReason { get; set; }
+ public string LastError { get; set; }
+ }
+}
diff --git a/AgroBase/AgroBase/Models/Components/AsyncTaskTimerModel.cs b/AgroBase/AgroBase/Models/Components/AsyncTaskTimerModel.cs
index a9bca38af..1eddf8f5a 100644
--- a/AgroBase/AgroBase/Models/Components/AsyncTaskTimerModel.cs
+++ b/AgroBase/AgroBase/Models/Components/AsyncTaskTimerModel.cs
@@ -1,5 +1,6 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -7,80 +8,200 @@ using System.Windows.Forms;
namespace AgroBase.Models
{
- public class AsyncTaskTimerModel : IDisposable
+ ///
+ /// Timer assíncrono de uso geral para os loops da aplicação.
+ ///
+ /// Garantias principais:
+ /// - nunca executa dois ticks simultaneamente;
+ /// - timeout não libera um novo tick enquanto o anterior ainda estiver vivo;
+ /// - Start/Stop/Restart são protegidos contra corridas;
+ /// - agendamento usa relógio monotônico;
+ /// - exceções do callback não encerram silenciosamente o loop;
+ /// - callbacks antigos Func<Task> continuam compatíveis;
+ /// - callbacks novos podem receber CancellationToken real.
+ ///
+ public sealed class AsyncTaskTimerModel : IDisposable
{
+ // Mantidos públicos por compatibilidade com o projeto atual.
+ // O código interno nunca itera essa lista sem antes criar um snapshot.
public static readonly object _timersLock = new object();
public static List Timers { get; set; } = new List();
- public static void PararTimers()
+ private static readonly AsyncLocal _timerEmExecucao =
+ new AsyncLocal();
+
+ private readonly object _lifecycleLock = new object();
+ private readonly object _metricsLock = new object();
+
+ private readonly Func _taskToExecute;
+ private readonly bool _callbackSupportsCancellation;
+ private readonly AsyncTaskTimerScheduleMode _scheduleMode;
+ private readonly bool _runImmediately;
+
+ private CancellationTokenSource _generationCts;
+ private Task _loopTask = Task.CompletedTask;
+
+ private int _stateValue = (int)TimerState.Created;
+ private long _generation;
+ private int _interval;
+ private int _timeout;
+ private int _isTicking;
+ private int _cleanupCompleted;
+
+ private readonly string _stackTrace;
+ private readonly string _id;
+
+ public Control _uiControl;
+
+ private DateTime _lastStartUtc = DateTime.MinValue;
+ private DateTime _lastCompletionUtc = DateTime.MinValue;
+ private DateTime _lastSuccessUtc = DateTime.MinValue;
+ private DateTime _lastFailureUtc = DateTime.MinValue;
+ private DateTime _lastTimeoutUtc = DateTime.MinValue;
+
+ private long _currentTickStartTimestamp;
+ private double _lastExecutionDurationMs;
+ private double _lastSuccessfulDurationMs;
+ private double _maxExecutionDurationMs;
+ private double _lastScheduleLatenessMs;
+ private double _maxScheduleLatenessMs;
+ private double _totalExecutionDurationMs;
+
+ private string _lastError;
+ private AsyncTaskTimerTickResult _lastResult = AsyncTaskTimerTickResult.None;
+
+ private long _ticksStarted;
+ private long _ticksFinished;
+ private long _ticksSucceeded;
+ private long _ticksFailed;
+ private long _ticksTimedOut;
+ private long _ticksCanceled;
+ private long _schedulesSkipped;
+ private long _overruns;
+
+ private int _consecutiveFailures;
+ private int _consecutiveTimeouts;
+ private int _consecutiveSuccesses;
+
+ ///
+ /// Mantido como propriedade configurável para compatibilidade e diagnóstico.
+ /// Em produção pode ser desativado individualmente por timer.
+ ///
+ public bool DebugMessages { get; set; } = false;
+
+ public TimerState State
{
- lock (_timersLock)
+ get { return (TimerState)Volatile.Read(ref _stateValue); }
+ }
+
+ public bool IsRunning { get { return State == TimerState.Running; } }
+ public bool IsStopped { get { return State == TimerState.Stopped; } }
+ public bool IsCreated { get { return State == TimerState.Created; } }
+ public bool IsDisposed { get { return State == TimerState.Disposed; } }
+ public bool IsTicking { get { return Volatile.Read(ref _isTicking) == 1; } }
+
+ public int Interval { get { return Volatile.Read(ref _interval); } }
+
+ public double IntervalHz
+ {
+ get
{
- foreach (var timer in Timers)
+ int interval = Interval;
+ return interval > 0 ? 1000.0 / interval : 0.0;
+ }
+ }
+
+ public int Timeout { get { return Volatile.Read(ref _timeout); } }
+ public string StackTrace { get { return _stackTrace; } }
+ public string Id { get { return _id; } }
+ public long Generation { get { return Interlocked.Read(ref _generation); } }
+ public AsyncTaskTimerScheduleMode ScheduleMode { get { return _scheduleMode; } }
+ public bool CallbackSupportsCancellation { get { return _callbackSupportsCancellation; } }
+
+ ///
+ /// Duração real do último callback, incluindo o tempo posterior ao timeout
+ /// caso o callback não tenha respeitado o cancelamento.
+ ///
+ public TimeSpan TimeTick
+ {
+ get
+ {
+ lock (_metricsLock)
{
- timer.Stop();
+ return TimeSpan.FromMilliseconds(_lastExecutionDurationMs);
}
}
}
-
- private TimerState _state = TimerState.Created;
- public TimerState State { get { return _state; } }
-
///
- /// Verifica se o timer foi iniciado e está executando.
+ /// Construtor compatível com todos os usos antigos Func<Task>.
+ /// O callback legado não recebe o token, portanto deve terminar por conta própria.
+ /// Mesmo após timeout, outro tick não será iniciado até ele realmente encerrar.
///
- public bool IsRunning => _state == TimerState.Running;
-
- ///
- /// Verifica se o timer está parado.
- ///
- public bool IsStopped => _state == TimerState.Stopped;
-
- ///
- /// Verifica se o timer foi criado, mas ainda não foi iniciado.
- ///
- public bool IsCreated => _state == TimerState.Created;
- public bool IsDisposed => _state == TimerState.Disposed;
-
- private CancellationTokenSource _cancellationTokenSource;
- private Func _taskToExecute;
- private int _interval; // Intervalo em milissegundos
- public int Interval { get { return _interval; } }
- public double IntervalHz { get { return (1.0 / ((double)_interval) * 1000.0); } }
- private string _stackTrace;
- public string StackTrace { get { return _stackTrace; } }
- private string _id;
- public string Id { get { return _id; } }
- public Control _uiControl;
- private int _timeout = 60000;
- private System.Timers.Timer _timer;
-
- private DateTime beginTask = DateTime.MinValue;
- private DateTime endTask = DateTime.MinValue;
- ///
- /// Obtém o tempo decorrido entre o início e o fim da tarefa.
- ///
- public TimeSpan TimeTick => endTask - beginTask;
-
- private bool _Ticking = false;
-
- private bool DebugMessages = true;
-
- ///
- /// Inicializa uma nova instância do AsyncTaskTimerService.
- ///
- /// A tarefa assíncrona que será executada periodicamente.
- /// Intervalo em milissegundos entre as execuções.
- /// Controle para executar tarefas relacionadas à UI. Opcional.
- public AsyncTaskTimerModel(string id, Func taskToExecute, int interval, Control uiControl = null, int timeout = 60000)
+ public AsyncTaskTimerModel(
+ string id,
+ Func taskToExecute,
+ int interval,
+ Control uiControl = null,
+ int timeout = 60000)
+ : this(
+ id,
+ taskToExecute == null
+ ? (Func)null
+ : (_ => taskToExecute()),
+ interval,
+ uiControl,
+ timeout,
+ AsyncTaskTimerScheduleMode.FixedDelay,
+ false,
+ false)
{
- //if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id));
- if (string.IsNullOrEmpty(id)) return;
- //if (taskToExecute == null) throw new ArgumentNullException(nameof(taskToExecute));
- if (taskToExecute == null) return;
- if (interval <= 0) throw new ArgumentException("Interval must be greater than zero.", nameof(interval));
- if (timeout <= 0) throw new ArgumentException("Timeout must be greater than zero.", nameof(timeout));
+ }
+
+ ///
+ /// Construtor recomendado para novos loops. O callback recebe cancelamento real.
+ ///
+ public AsyncTaskTimerModel(
+ string id,
+ Func taskToExecute,
+ int interval,
+ Control uiControl = null,
+ int timeout = 60000,
+ AsyncTaskTimerScheduleMode scheduleMode = AsyncTaskTimerScheduleMode.FixedDelay,
+ bool runImmediately = false)
+ : this(
+ id,
+ taskToExecute,
+ interval,
+ uiControl,
+ timeout,
+ scheduleMode,
+ runImmediately,
+ true)
+ {
+ }
+
+ private AsyncTaskTimerModel(
+ string id,
+ Func taskToExecute,
+ int interval,
+ Control uiControl,
+ int timeout,
+ AsyncTaskTimerScheduleMode scheduleMode,
+ bool runImmediately,
+ bool callbackSupportsCancellation)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ throw new ArgumentException("O id do timer é obrigatório.", nameof(id));
+
+ if (taskToExecute == null)
+ throw new ArgumentNullException(nameof(taskToExecute));
+
+ if (interval <= 0)
+ throw new ArgumentOutOfRangeException(nameof(interval), "O intervalo deve ser maior que zero.");
+
+ if (timeout <= 0)
+ throw new ArgumentOutOfRangeException(nameof(timeout), "O timeout deve ser maior que zero.");
_id = id;
_stackTrace = ObterIdDoChamador();
@@ -88,299 +209,1028 @@ namespace AgroBase.Models
_interval = interval;
_timeout = timeout;
_uiControl = uiControl;
-
- _timer = new System.Timers.Timer();
- _timer.Interval = interval;
- _timer.AutoReset = false;
- _timer.Elapsed += _timer_Elapsed;
- _timer.Disposed += _timer_Disposed;
+ _scheduleMode = scheduleMode;
+ _runImmediately = runImmediately;
+ _callbackSupportsCancellation = callbackSupportsCancellation;
lock (_timersLock)
{
+ if (Timers == null)
+ Timers = new List();
+
Timers.Add(this);
}
}
- private void _timer_Disposed(object sender, EventArgs e)
- {
- _state = TimerState.Disposed;
- }
-
- private async void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
- {
- System.Timers.Timer tmr = sender as System.Timers.Timer;
- if (IsDisposed) return;
- tmr.Stop();
-
- if (_Ticking)
- {
- if (_state == TimerState.Running)
- {
- tmr.Start();
- }
- return;
- }
-
- _Ticking = true;
-
- beginTask = DateTime.Now;
-
- try
- {
- // Define a tarefa a ser executada
- var taskToRun = _uiControl != null
- ? FuncoesGlobais.ExecutarMetodoComVerificacaoCrossThreadAsync(_uiControl, _taskToExecute)
- : _taskToExecute();
-
- // Aguarda a conclusão da tarefa ou o timeout
- var completedTask = await Task.WhenAny(taskToRun, Task.Delay(_timeout));
-
- if (completedTask == taskToRun)
- {
- // A tarefa foi concluída dentro do limite
- await taskToRun;
- }
- else
- {
- // Timeout atingido
- ExibirConsole($"TIMEOUT - tarefa excedeu {_timeout}ms");
- //Restart();
- }
-
- endTask = DateTime.Now;
- }
- catch (OperationCanceledException)
- {
- // Cancelamento solicitado
- ExibirConsole($"CANCEL");
- return;
- }
- catch (Exception ex)
- {
- // Log de erros inesperados
- ExibirConsole($"ERROR");
- Console.WriteLine($"Erro no timer {_id}: {ex.Message}");
- }
- finally
- {
- _Ticking = false;
- if (_state == TimerState.Running)
- {
- try { if (!IsDisposed) tmr.Start(); } catch { }
- }
- }
- }
-
///
- /// Inicia o timer assíncrono.
+ /// Solicita a parada de todos os timers registrados.
+ /// Não bloqueia a thread chamadora. Use PararTimersAsync no shutdown
+ /// quando for necessário aguardar o encerramento real dos callbacks.
///
- public void Start()
+ public static void PararTimers()
{
- if (VerificaDisposed()) return;
- if (_state == TimerState.Running) return;
+ AsyncTaskTimerModel[] snapshot = GetTimersSnapshot();
- ExibirConsole($"START");
-
- _state = TimerState.Running;
-
- _cancellationTokenSource = new CancellationTokenSource();
- //Task.Run(() => TimerLoopAsync(_cancellationTokenSource.Token));
- _timer.Start();
- }
-
- ///
- /// Para o timer assíncrono.
- ///
- public void Stop()
- {
- if (VerificaDisposed()) return;
- if (_state != TimerState.Running) return;
-
- _cancellationTokenSource.Cancel();
- _cancellationTokenSource = null;
-
- _state = TimerState.Stopped;
-
- ExibirConsole($"STOP");
-
- _timer.Stop();
- }
-
- ///
- /// Reinicia o timer assíncrono.
- ///
- public void Restart()
- {
- if (VerificaDisposed()) return;
-
- Stop();
- Start();
-
- ExibirConsole($"RESTART");
- }
-
- ///
- /// Alterna o status do timer.
- ///
- public void Toogle()
- {
- if (VerificaDisposed()) return;
-
- if (IsRunning)
+ foreach (AsyncTaskTimerModel timer in snapshot)
{
- Stop();
- }
- else
- {
- Start();
- }
-
- ExibirConsole($"TOOGLE");
- }
-
- ///
- /// Define o intervalo entre as execuções.
- ///
- /// Novo intervalo em milissegundos.
- public void SetInterval(int interval)
- {
- if (VerificaDisposed()) return;
-
- if (interval <= 0) throw new ArgumentException("Interval must be greater than zero.", nameof(interval));
- _interval = interval;
-
- _timer.Interval = _interval;
- }
-
- ///
- /// Loop interno do timer com timeout na execução da tarefa.
- ///
- private async Task TimerLoopAsync(CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested && Id == "tmrMonitoramentoRecursos")
- {
- if (_Ticking)
- {
- return;
- }
-
- _Ticking = true;
-
- beginTask = DateTime.Now;
-
try
{
- // Cria um token de cancelamento específico para o timeout
- var timeoutCts = new CancellationTokenSource(_timeout);
- var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
-
- // Define a tarefa a ser executada
- var taskToRun = _uiControl != null
- ? FuncoesGlobais.ExecutarMetodoComVerificacaoCrossThreadAsync(_uiControl, _taskToExecute)
- : _taskToExecute();
-
- // Aguarda a conclusão da tarefa ou o timeout
- var completedTask = await Task.WhenAny(taskToRun, Task.Delay(_timeout, linkedTokenSource.Token));
-
- if (completedTask == taskToRun)
- {
- // A tarefa foi concluída dentro do limite
- await taskToRun;
- }
- else
- {
- // Timeout atingido
- ExibirConsole($"TIMEOUT - tarefa excedeu {_timeout}ms");
- //Restart();
- }
-
- endTask = DateTime.Now;
- }
- catch (OperationCanceledException)
- {
- // Cancelamento solicitado
- ExibirConsole($"CANCEL");
- break;
+ timer.Stop();
}
catch (Exception ex)
{
- // Log de erros inesperados
- ExibirConsole($"ERROR");
- Console.WriteLine($"Erro no timer {_id}: {ex.Message}");
- }
- finally
- {
- // Garante que o intervalo é respeitado entre execuções
- await Task.Delay(_interval, cancellationToken);
- _Ticking = false;
- //_ticks++;
+ Variaveis.MostrarLog($"[AynscTaskTimer.PararTimers] Erro ao solicitar parada do timer '{timer?.Id}': {ex}");
}
}
-
- ExibirConsole($"ENCERRADO");
}
///
- /// Descarta os recursos após finalizar o uso para liberar a memória.
+ /// Para todos os timers e aguarda cada loop realmente terminar.
+ /// Este é o método recomendado para o encerramento da aplicação.
+ ///
+ public static async Task PararTimersAsync()
+ {
+ AsyncTaskTimerModel[] snapshot = GetTimersSnapshot();
+
+ Task[] tasks = snapshot
+ .Where(x => x != null)
+ .Select(x => x.StopAsync())
+ .ToArray();
+
+ if (tasks.Length > 0)
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+
+ public static AsyncTaskTimerModel[] GetTimersSnapshot()
+ {
+ lock (_timersLock)
+ {
+ if (Timers == null)
+ return Array.Empty();
+
+ return Timers
+ .Where(x => x != null)
+ .ToArray();
+ }
+ }
+
+ ///
+ /// Inicia o timer. O primeiro tick respeita o intervalo, como na classe antiga,
+ /// exceto quando runImmediately=true no construtor com CancellationToken.
+ ///
+ public void Start()
+ {
+ Task previousLoop;
+ CancellationTokenSource cts;
+ long generation;
+
+ lock (_lifecycleLock)
+ {
+ ThrowIfDisposed();
+
+ if (State == TimerState.Running)
+ return;
+
+ previousLoop = _loopTask ?? Task.CompletedTask;
+ cts = new CancellationTokenSource();
+ generation = Interlocked.Increment(ref _generation);
+
+ _generationCts = cts;
+ Volatile.Write(ref _stateValue, (int)TimerState.Running);
+
+ _loopTask = Task.Run(async () =>
+ {
+ // Uma nova geração sempre espera a geração anterior encerrar.
+ // Isso impede overlap até quando Stop/Start são chamados em sequência.
+ try
+ {
+ await previousLoop.ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ ExibirConsole($"LOOP ANTERIOR FINALIZOU COM ERRO: {ex}");
+ }
+
+ if (!IsGenerationActive(generation, cts.Token))
+ return;
+
+ await RunGenerationAsync(generation, cts).ConfigureAwait(false);
+ });
+ }
+
+ ExibirConsole($"START geração={generation} modo={_scheduleMode} intervalo={Interval}ms");
+ }
+
+ ///
+ /// Solicita parada sem bloquear a thread chamadora.
+ /// O callback atual recebe cancelamento, quando suportado, e nenhum novo tick começa.
+ ///
+ public void Stop()
+ {
+ RequestStop();
+ }
+
+ ///
+ /// Solicita parada e aguarda o callback/loop atual finalizar de verdade.
+ /// Se um callback legado ignorar timeout e nunca retornar, este método também aguardará,
+ /// preservando a garantia de que não haverá uma segunda execução simultânea.
+ ///
+ public async Task StopAsync()
+ {
+ Task loop = RequestStop();
+
+ // Evita auto-deadlock caso um callback tente parar e aguardar o próprio timer.
+ if (ReferenceEquals(_timerEmExecucao.Value, this))
+ return;
+
+ if (loop != null)
+ {
+ try
+ {
+ await loop.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Encerramento normal.
+ }
+ catch (Exception ex)
+ {
+ ExibirConsole($"ERRO AO AGUARDAR STOP: {ex}");
+ }
+ }
+ }
+
+ public void Restart()
+ {
+ ThrowIfDisposed();
+ Stop();
+ Start();
+ ExibirConsole("RESTART solicitado");
+ }
+
+ public async Task RestartAsync()
+ {
+ ThrowIfDisposed();
+ await StopAsync().ConfigureAwait(false);
+ Start();
+ ExibirConsole("RESTART concluído");
+ }
+
+ ///
+ /// Mantido com a grafia antiga para compatibilidade.
+ ///
+ public void Toogle()
+ {
+ Toggle();
+ }
+
+ public void Toggle()
+ {
+ ThrowIfDisposed();
+
+ if (IsRunning)
+ Stop();
+ else
+ Start();
+ }
+
+ public void SetInterval(int interval)
+ {
+ ThrowIfDisposed();
+
+ if (interval <= 0)
+ throw new ArgumentOutOfRangeException(nameof(interval), "O intervalo deve ser maior que zero.");
+
+ Volatile.Write(ref _interval, interval);
+ ExibirConsole($"INTERVAL alterado para {interval}ms");
+ }
+
+ public void SetTimeout(int timeout)
+ {
+ ThrowIfDisposed();
+
+ if (timeout <= 0)
+ throw new ArgumentOutOfRangeException(nameof(timeout), "O timeout deve ser maior que zero.");
+
+ Volatile.Write(ref _timeout, timeout);
+ ExibirConsole($"TIMEOUT alterado para {timeout}ms");
+ }
+
+ private Task RequestStop()
+ {
+ CancellationTokenSource cts = null;
+ Task loop;
+ bool changed = false;
+
+ lock (_lifecycleLock)
+ {
+ if (IsDisposed)
+ return _loopTask ?? Task.CompletedTask;
+
+ loop = _loopTask ?? Task.CompletedTask;
+
+ if (State == TimerState.Running)
+ {
+ Volatile.Write(ref _stateValue, (int)TimerState.Stopped);
+ Interlocked.Increment(ref _generation);
+ cts = _generationCts;
+ changed = true;
+ }
+ }
+
+ if (cts != null)
+ {
+ try
+ {
+ cts.Cancel();
+ }
+ catch (ObjectDisposedException)
+ {
+ // A geração já terminou entre o snapshot e o cancelamento.
+ }
+ }
+
+ if (changed)
+ ExibirConsole("STOP solicitado");
+
+ return loop;
+ }
+
+ private async Task RunGenerationAsync(long generation, CancellationTokenSource generationCts)
+ {
+ CancellationToken token = generationCts.Token;
+
+ try
+ {
+ if (_scheduleMode == AsyncTaskTimerScheduleMode.FixedRateSkipMissed)
+ await RunFixedRateAsync(generation, token).ConfigureAwait(false);
+ else
+ await RunFixedDelayAsync(generation, token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (token.IsCancellationRequested)
+ {
+ // Encerramento esperado.
+ }
+ catch (Exception ex)
+ {
+ // Um erro do motor interno nunca deve desaparecer silenciosamente.
+ RegisterInfrastructureFailure(ex);
+ ExibirConsole($"ERRO FATAL NO LOOP: {ex}");
+ }
+ finally
+ {
+ try
+ {
+ generationCts.Dispose();
+ }
+ catch
+ {
+ }
+
+ lock (_lifecycleLock)
+ {
+ if (ReferenceEquals(_generationCts, generationCts))
+ _generationCts = null;
+
+ // Só a geração ainda vigente pode alterar o estado.
+ if (Generation == generation && State == TimerState.Running)
+ Volatile.Write(ref _stateValue, (int)TimerState.Stopped);
+ }
+
+ ExibirConsole($"ENCERRADO geração={generation}");
+ }
+ }
+
+ private async Task RunFixedDelayAsync(long generation, CancellationToken token)
+ {
+ bool firstTick = true;
+
+ while (IsGenerationActive(generation, token))
+ {
+ if (!(firstTick && _runImmediately))
+ {
+ int delayMs = Interval;
+ await Task.Delay(delayMs, token).ConfigureAwait(false);
+ }
+
+ firstTick = false;
+
+ if (!IsGenerationActive(generation, token))
+ break;
+
+ RegisterScheduleLateness(0.0);
+ await ExecuteOneTickAsync(generation, token).ConfigureAwait(false);
+ }
+ }
+
+ private async Task RunFixedRateAsync(long generation, CancellationToken token)
+ {
+ long nextDue = Stopwatch.GetTimestamp();
+ int intervalAtSchedule = Interval;
+
+ if (!_runImmediately)
+ nextDue += MillisecondsToStopwatchTicks(intervalAtSchedule);
+
+ while (IsGenerationActive(generation, token))
+ {
+ await DelayUntilAsync(nextDue, token).ConfigureAwait(false);
+
+ if (!IsGenerationActive(generation, token))
+ break;
+
+ long actualStart = Stopwatch.GetTimestamp();
+ double latenessMs = StopwatchTicksToMilliseconds(Math.Max(0L, actualStart - nextDue));
+ RegisterScheduleLateness(latenessMs);
+
+ await ExecuteOneTickAsync(generation, token).ConfigureAwait(false);
+
+ if (!IsGenerationActive(generation, token))
+ break;
+
+ int currentInterval = Interval;
+ long now = Stopwatch.GetTimestamp();
+
+ // Se o intervalo mudou, inicia uma nova cadência a partir de agora.
+ if (currentInterval != intervalAtSchedule)
+ {
+ intervalAtSchedule = currentInterval;
+ nextDue = now + MillisecondsToStopwatchTicks(currentInterval);
+ continue;
+ }
+
+ long intervalTicks = MillisecondsToStopwatchTicks(currentInterval);
+ nextDue += intervalTicks;
+
+ if (nextDue <= now)
+ {
+ long skipped = ((now - nextDue) / intervalTicks) + 1L;
+ nextDue += skipped * intervalTicks;
+ Interlocked.Add(ref _schedulesSkipped, skipped);
+ Interlocked.Increment(ref _overruns);
+ }
+ }
+ }
+
+ private async Task ExecuteOneTickAsync(long generation, CancellationToken runToken)
+ {
+ if (Interlocked.CompareExchange(ref _isTicking, 1, 0) != 0)
+ {
+ // Esta condição não deveria ocorrer. Se ocorrer, é registrada e o tick é descartado.
+ Interlocked.Increment(ref _schedulesSkipped);
+ ExibirConsole("TICK DESCARTADO: execução anterior ainda ativa");
+ return;
+ }
+
+ long startedTimestamp = Stopwatch.GetTimestamp();
+ Interlocked.Exchange(ref _currentTickStartTimestamp, startedTimestamp);
+ Interlocked.Increment(ref _ticksStarted);
+
+ lock (_metricsLock)
+ {
+ _lastStartUtc = DateTime.UtcNow;
+ _lastResult = AsyncTaskTimerTickResult.Running;
+ }
+
+ bool timedOut = false;
+ bool canceledByStop = false;
+ Exception callbackException = null;
+
+ CancellationTokenSource tickCts = null;
+ CancellationTokenSource timeoutDelayCts = null;
+
+ try
+ {
+ if (!IsGenerationActive(generation, runToken))
+ {
+ canceledByStop = true;
+ return;
+ }
+
+ tickCts = CancellationTokenSource.CreateLinkedTokenSource(runToken);
+ timeoutDelayCts = new CancellationTokenSource();
+
+ Task callbackTask;
+
+ try
+ {
+ _timerEmExecucao.Value = this;
+ callbackTask = ExecuteCallbackAsync(tickCts.Token) ?? Task.CompletedTask;
+ }
+ catch (Exception ex)
+ {
+ callbackTask = Task.FromException(ex);
+ }
+
+ Task timeoutTask = Task.Delay(Timeout, timeoutDelayCts.Token);
+ Task stopTask = Task.Delay(System.Threading.Timeout.Infinite, runToken);
+
+ Task first = await Task.WhenAny(callbackTask, timeoutTask, stopTask).ConfigureAwait(false);
+
+ if (first == callbackTask)
+ {
+ try
+ {
+ timeoutDelayCts.Cancel();
+ }
+ catch
+ {
+ }
+ }
+ else if (first == stopTask || runToken.IsCancellationRequested)
+ {
+ canceledByStop = true;
+ SafeCancel(tickCts);
+ }
+ else
+ {
+ timedOut = true;
+ SafeCancel(tickCts);
+ RegisterTimeout();
+ ExibirConsole(
+ $"TIMEOUT - tarefa excedeu {Timeout}ms. " +
+ "Nenhum novo tick será iniciado até o callback realmente terminar.");
+ }
+
+ // Esta espera é deliberada. Mesmo após timeout/cancelamento, não liberamos
+ // outro tick enquanto o callback anterior ainda existir.
+ try
+ {
+ await callbackTask.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (tickCts.IsCancellationRequested)
+ {
+ // Cancelamento cooperativo esperado.
+ }
+ catch (Exception ex)
+ {
+ callbackException = ex;
+ }
+ }
+ finally
+ {
+ _timerEmExecucao.Value = null;
+
+ if (timeoutDelayCts != null)
+ {
+ try { timeoutDelayCts.Cancel(); } catch { }
+ timeoutDelayCts.Dispose();
+ }
+
+ if (tickCts != null)
+ tickCts.Dispose();
+
+ long finishedTimestamp = Stopwatch.GetTimestamp();
+ double durationMs = StopwatchTicksToMilliseconds(finishedTimestamp - startedTimestamp);
+
+ RegisterTickCompletion(
+ durationMs,
+ timedOut,
+ canceledByStop,
+ callbackException);
+
+ Interlocked.Exchange(ref _currentTickStartTimestamp, 0L);
+ Interlocked.Exchange(ref _isTicking, 0);
+ }
+ }
+
+ private Task ExecuteCallbackAsync(CancellationToken token)
+ {
+ if (_uiControl == null)
+ return _taskToExecute(token);
+
+ if (_uiControl.IsDisposed || _uiControl.Disposing)
+ throw new ObjectDisposedException(
+ _uiControl.Name,
+ $"O controle de UI associado ao timer '{Id}' foi descartado.");
+
+ // Mantém compatibilidade com o helper já usado pelo projeto.
+ return FuncoesGlobais.ExecutarMetodoComVerificacaoCrossThreadAsync(
+ _uiControl,
+ () => _taskToExecute(token));
+ }
+
+ private static async Task DelayUntilAsync(long dueTimestamp, CancellationToken token)
+ {
+ while (true)
+ {
+ token.ThrowIfCancellationRequested();
+
+ long remainingTicks = dueTimestamp - Stopwatch.GetTimestamp();
+ if (remainingTicks <= 0)
+ return;
+
+ double remainingMs = StopwatchTicksToMilliseconds(remainingTicks);
+
+ // Task.Delay possui granularidade limitada no Windows. Um pequeno laço final
+ // reduz deriva sem transformar o timer em busy-wait permanente.
+ if (remainingMs > 3.0)
+ {
+ int delay = Math.Max(1, (int)Math.Floor(remainingMs - 1.0));
+ await Task.Delay(delay, token).ConfigureAwait(false);
+ }
+ else
+ {
+ await Task.Yield();
+ }
+ }
+ }
+
+ private bool IsGenerationActive(long generation, CancellationToken token)
+ {
+ return
+ !token.IsCancellationRequested &&
+ !IsDisposed &&
+ IsRunning &&
+ Generation == generation;
+ }
+
+ private void RegisterTimeout()
+ {
+ Interlocked.Increment(ref _ticksTimedOut);
+ Interlocked.Increment(ref _consecutiveTimeouts);
+ Interlocked.Exchange(ref _consecutiveSuccesses, 0);
+
+ lock (_metricsLock)
+ {
+ _lastTimeoutUtc = DateTime.UtcNow;
+ }
+ }
+
+ private void RegisterTickCompletion(
+ double durationMs,
+ bool timedOut,
+ bool canceledByStop,
+ Exception callbackException)
+ {
+ Interlocked.Increment(ref _ticksFinished);
+
+ lock (_metricsLock)
+ {
+ _lastCompletionUtc = DateTime.UtcNow;
+ _lastExecutionDurationMs = durationMs;
+ _totalExecutionDurationMs += durationMs;
+
+ if (durationMs > _maxExecutionDurationMs)
+ _maxExecutionDurationMs = durationMs;
+ }
+
+ if (callbackException != null)
+ {
+ Interlocked.Increment(ref _ticksFailed);
+ Interlocked.Increment(ref _consecutiveFailures);
+ Interlocked.Exchange(ref _consecutiveSuccesses, 0);
+
+ lock (_metricsLock)
+ {
+ _lastFailureUtc = DateTime.UtcNow;
+ _lastError = callbackException.ToString();
+ _lastResult = AsyncTaskTimerTickResult.Failed;
+ }
+
+ ExibirConsole($"ERROR: {callbackException}");
+ return;
+ }
+
+ if (timedOut)
+ {
+ // O timeout já foi contabilizado no instante em que ocorreu.
+ Interlocked.Increment(ref _consecutiveFailures);
+
+ lock (_metricsLock)
+ {
+ _lastResult = AsyncTaskTimerTickResult.TimedOut;
+ }
+
+ return;
+ }
+
+ if (canceledByStop)
+ {
+ Interlocked.Increment(ref _ticksCanceled);
+
+ lock (_metricsLock)
+ {
+ _lastResult = AsyncTaskTimerTickResult.Canceled;
+ }
+
+ return;
+ }
+
+ Interlocked.Increment(ref _ticksSucceeded);
+ Interlocked.Increment(ref _consecutiveSuccesses);
+ Interlocked.Exchange(ref _consecutiveFailures, 0);
+ Interlocked.Exchange(ref _consecutiveTimeouts, 0);
+
+ lock (_metricsLock)
+ {
+ _lastSuccessUtc = DateTime.UtcNow;
+ _lastSuccessfulDurationMs = durationMs;
+ _lastError = null;
+ _lastResult = AsyncTaskTimerTickResult.Succeeded;
+ }
+ }
+
+ private void RegisterScheduleLateness(double latenessMs)
+ {
+ lock (_metricsLock)
+ {
+ _lastScheduleLatenessMs = latenessMs;
+
+ if (latenessMs > _maxScheduleLatenessMs)
+ _maxScheduleLatenessMs = latenessMs;
+ }
+ }
+
+ private void RegisterInfrastructureFailure(Exception ex)
+ {
+ Interlocked.Increment(ref _ticksFailed);
+ Interlocked.Increment(ref _consecutiveFailures);
+ Interlocked.Exchange(ref _consecutiveSuccesses, 0);
+
+ lock (_metricsLock)
+ {
+ _lastFailureUtc = DateTime.UtcNow;
+ _lastError = "Falha interna do timer: " + ex;
+ _lastResult = AsyncTaskTimerTickResult.InfrastructureFailure;
+ }
+ }
+
+ public AsyncTaskTimerMetrics GetMetrics()
+ {
+ double lastDuration;
+ double lastSuccessfulDuration;
+ double maxDuration;
+ double lastLateness;
+ double maxLateness;
+ double totalDuration;
+ DateTime lastStart;
+ DateTime lastCompletion;
+ DateTime lastSuccess;
+ DateTime lastFailure;
+ DateTime lastTimeout;
+ string lastError;
+ AsyncTaskTimerTickResult lastResult;
+
+ lock (_metricsLock)
+ {
+ lastDuration = _lastExecutionDurationMs;
+ lastSuccessfulDuration = _lastSuccessfulDurationMs;
+ maxDuration = _maxExecutionDurationMs;
+ lastLateness = _lastScheduleLatenessMs;
+ maxLateness = _maxScheduleLatenessMs;
+ totalDuration = _totalExecutionDurationMs;
+ lastStart = _lastStartUtc;
+ lastCompletion = _lastCompletionUtc;
+ lastSuccess = _lastSuccessUtc;
+ lastFailure = _lastFailureUtc;
+ lastTimeout = _lastTimeoutUtc;
+ lastError = _lastError;
+ lastResult = _lastResult;
+ }
+
+ long finished = Interlocked.Read(ref _ticksFinished);
+ double averageDuration = finished > 0 ? totalDuration / finished : 0.0;
+
+ return new AsyncTaskTimerMetrics
+ {
+ Id = Id,
+ Origin = StackTrace,
+ State = State,
+ ScheduleMode = ScheduleMode,
+ IntervalMs = Interval,
+ TimeoutMs = Timeout,
+ CallbackSupportsCancellation = CallbackSupportsCancellation,
+ Generation = Generation,
+ IsTicking = IsTicking,
+
+ TicksStarted = Interlocked.Read(ref _ticksStarted),
+ TicksFinished = finished,
+ TicksSucceeded = Interlocked.Read(ref _ticksSucceeded),
+ TicksFailed = Interlocked.Read(ref _ticksFailed),
+ TicksTimedOut = Interlocked.Read(ref _ticksTimedOut),
+ TicksCanceled = Interlocked.Read(ref _ticksCanceled),
+ SchedulesSkipped = Interlocked.Read(ref _schedulesSkipped),
+ Overruns = Interlocked.Read(ref _overruns),
+
+ ConsecutiveSuccesses = Volatile.Read(ref _consecutiveSuccesses),
+ ConsecutiveFailures = Volatile.Read(ref _consecutiveFailures),
+ ConsecutiveTimeouts = Volatile.Read(ref _consecutiveTimeouts),
+
+ LastExecutionDurationMs = lastDuration,
+ LastSuccessfulDurationMs = lastSuccessfulDuration,
+ AverageExecutionDurationMs = averageDuration,
+ MaxExecutionDurationMs = maxDuration,
+ CurrentExecutionDurationMs = GetCurrentExecutionDurationMs(),
+
+ LastScheduleLatenessMs = lastLateness,
+ MaxScheduleLatenessMs = maxLateness,
+
+ LastStartUtc = lastStart,
+ LastCompletionUtc = lastCompletion,
+ LastSuccessUtc = lastSuccess,
+ LastFailureUtc = lastFailure,
+ LastTimeoutUtc = lastTimeout,
+ LastCompletionAgeMs = AgeMilliseconds(lastCompletion),
+
+ LastResult = lastResult,
+ LastError = lastError,
+ Healthy = CalculateHealthy(lastCompletion, lastResult)
+ };
+ }
+
+ private bool CalculateHealthy(DateTime lastCompletionUtc, AsyncTaskTimerTickResult lastResult)
+ {
+ if (IsDisposed)
+ return false;
+
+ if (!IsRunning)
+ return State == TimerState.Created || State == TimerState.Stopped;
+
+ if (lastResult == AsyncTaskTimerTickResult.Failed ||
+ lastResult == AsyncTaskTimerTickResult.InfrastructureFailure ||
+ Volatile.Read(ref _consecutiveTimeouts) > 0)
+ {
+ return false;
+ }
+
+ if (IsTicking)
+ {
+ double currentDuration = GetCurrentExecutionDurationMs();
+ return currentDuration <= Math.Max(Timeout * 1.25, Interval * 3.0);
+ }
+
+ if (lastCompletionUtc == DateTime.MinValue)
+ return true;
+
+ double allowedAgeMs = Math.Max(Interval * 4.0, Timeout + Interval * 2.0);
+ return AgeMilliseconds(lastCompletionUtc) <= allowedAgeMs;
+ }
+
+ private double GetCurrentExecutionDurationMs()
+ {
+ long start = Interlocked.Read(ref _currentTickStartTimestamp);
+ if (start <= 0 || !IsTicking)
+ return 0.0;
+
+ return StopwatchTicksToMilliseconds(Stopwatch.GetTimestamp() - start);
+ }
+
+ private static double AgeMilliseconds(DateTime utc)
+ {
+ if (utc == DateTime.MinValue)
+ return -1.0;
+
+ return Math.Max(0.0, (DateTime.UtcNow - utc).TotalMilliseconds);
+ }
+
+ ///
+ /// Solicita descarte sem bloquear a thread chamadora.
+ /// Para shutdown determinístico prefira DisposeAsync().
///
public void Dispose()
{
- if (VerificaDisposed()) return;
+ Task loopToObserve;
+ CancellationTokenSource cts;
- // Cancela o token de cancelamento
- Stop();
-
- // Remove o timer da lista estática
- lock (_timersLock)
+ lock (_lifecycleLock)
{
- Timers.Remove(this);
+ if (IsDisposed)
+ return;
+
+ Volatile.Write(ref _stateValue, (int)TimerState.Disposed);
+ Interlocked.Increment(ref _generation);
+
+ cts = _generationCts;
+ loopToObserve = _loopTask ?? Task.CompletedTask;
}
- // Libera o CancellationTokenSource
- _cancellationTokenSource?.Dispose();
+ SafeCancel(cts);
+ RemoveFromRegistry();
- _state = TimerState.Disposed;
+ // Não bloqueia a UI nem cria deadlock. Os recursos restantes são limpos
+ // quando o loop realmente encerrar.
+ _ = loopToObserve.ContinueWith(
+ _ => CompleteCleanup(),
+ CancellationToken.None,
+ TaskContinuationOptions.ExecuteSynchronously,
+ TaskScheduler.Default);
- ExibirConsole($"DISPOSED");
-
- _timer?.Dispose();
+ ExibirConsole("DISPOSE solicitado");
}
+ public async Task DisposeAsync()
+ {
+ Task loopToAwait;
+ CancellationTokenSource cts;
+
+ lock (_lifecycleLock)
+ {
+ if (!IsDisposed)
+ {
+ Volatile.Write(ref _stateValue, (int)TimerState.Disposed);
+ Interlocked.Increment(ref _generation);
+ }
+
+ cts = _generationCts;
+ loopToAwait = _loopTask ?? Task.CompletedTask;
+ }
+
+ SafeCancel(cts);
+ RemoveFromRegistry();
+
+ if (!ReferenceEquals(_timerEmExecucao.Value, this))
+ {
+ try
+ {
+ await loopToAwait.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception ex)
+ {
+ ExibirConsole($"ERRO DURANTE DISPOSE ASYNC: {ex}");
+ }
+ }
+
+ CompleteCleanup();
+ ExibirConsole("DISPOSED");
+ }
+
+ private void CompleteCleanup()
+ {
+ if (Interlocked.Exchange(ref _cleanupCompleted, 1) == 1)
+ return;
+
+ CancellationTokenSource cts;
+
+ lock (_lifecycleLock)
+ {
+ cts = _generationCts;
+ _generationCts = null;
+ }
+
+ if (cts != null)
+ {
+ try { cts.Dispose(); } catch { }
+ }
+ }
+
+ private void RemoveFromRegistry()
+ {
+ lock (_timersLock)
+ {
+ Timers?.Remove(this);
+ }
+ }
+
+ private static void SafeCancel(CancellationTokenSource cts)
+ {
+ if (cts == null)
+ return;
+
+ try
+ {
+ cts.Cancel();
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (IsDisposed)
+ throw new ObjectDisposedException(
+ nameof(AsyncTaskTimerModel),
+ $"O timer '{Id}' já foi descartado.");
+ }
private string ObterIdDoChamador()
{
- var stackTrace = new System.Diagnostics.StackTrace();
- var frame = stackTrace.GetFrame(2); // Obter o chamador que instanciou este objeto
- var method = frame.GetMethod();
- return $"{method.DeclaringType?.Name}.{method.Name}";
+ try
+ {
+ StackTrace stackTrace = new StackTrace();
+
+ // Ignora frames pertencentes à própria classe para encontrar
+ // o ponto real que instanciou o timer.
+ foreach (StackFrame frame in stackTrace.GetFrames() ?? Array.Empty())
+ {
+ var method = frame.GetMethod();
+ Type declaringType = method?.DeclaringType;
+
+ if (declaringType == null || declaringType == typeof(AsyncTaskTimerModel))
+ continue;
+
+ return $"{declaringType.FullName}.{method.Name}";
+ }
+ }
+ catch
+ {
+ }
+
+ return "origem_desconhecida";
}
private void ExibirConsole(string msg)
{
- if (DebugMessages)
- {
- Console.WriteLine($"AsyncTaskTimer id = {_stackTrace} -> {Id} - {msg}");
- }
+ if (!DebugMessages)
+ return;
+
+ Variaveis.MostrarLog(
+ $"[AsyncTaskTimer.ExibirConsole] origem={_stackTrace} id={Id} estado={State} -> {msg}");
}
- private bool VerificaDisposed()
+ private static long MillisecondsToStopwatchTicks(int milliseconds)
{
- bool _disposed = _state == TimerState.Disposed;
- if (_disposed)
- ExibirConsole($"O timer {Id} já foi descartado!");
- return _disposed;
+ return Math.Max(
+ 1L,
+ (long)Math.Round(milliseconds * (double)Stopwatch.Frequency / 1000.0));
+ }
+
+ private static double StopwatchTicksToMilliseconds(long ticks)
+ {
+ return ticks * 1000.0 / Stopwatch.Frequency;
}
-
}
public enum TimerState
{
- Created, // Apenas criado, mas não iniciado
- Running, // Executando ativamente
- Stopped, // Parado após ser iniciado
- Disposed, // Descartado
+ Created,
+ Running,
+ Stopped,
+ Disposed,
}
+ public enum AsyncTaskTimerScheduleMode
+ {
+ ///
+ /// Comportamento compatível com a classe antiga:
+ /// callback termina, depois aguarda o intervalo completo.
+ ///
+ FixedDelay = 0,
+ ///
+ /// Mantém cadência monotônica. Se houver atraso, pula prazos perdidos
+ /// e nunca tenta compensar executando em rajada.
+ ///
+ FixedRateSkipMissed = 1,
+ }
+ public enum AsyncTaskTimerTickResult
+ {
+ None = 0,
+ Running = 1,
+ Succeeded = 2,
+ Failed = 3,
+ TimedOut = 4,
+ Canceled = 5,
+ InfrastructureFailure = 6,
+ }
+
+ public sealed class AsyncTaskTimerMetrics
+ {
+ public string Id { get; set; }
+ public string Origin { get; set; }
+ public TimerState State { get; set; }
+ public AsyncTaskTimerScheduleMode ScheduleMode { get; set; }
+ public int IntervalMs { get; set; }
+ public int TimeoutMs { get; set; }
+ public bool CallbackSupportsCancellation { get; set; }
+ public long Generation { get; set; }
+ public bool IsTicking { get; set; }
+
+ public long TicksStarted { get; set; }
+ public long TicksFinished { get; set; }
+ public long TicksSucceeded { get; set; }
+ public long TicksFailed { get; set; }
+ public long TicksTimedOut { get; set; }
+ public long TicksCanceled { get; set; }
+ public long SchedulesSkipped { get; set; }
+ public long Overruns { get; set; }
+
+ public int ConsecutiveSuccesses { get; set; }
+ public int ConsecutiveFailures { get; set; }
+ public int ConsecutiveTimeouts { get; set; }
+
+ public double LastExecutionDurationMs { get; set; }
+ public double LastSuccessfulDurationMs { get; set; }
+ public double AverageExecutionDurationMs { get; set; }
+ public double MaxExecutionDurationMs { get; set; }
+ public double CurrentExecutionDurationMs { get; set; }
+
+ public double LastScheduleLatenessMs { get; set; }
+ public double MaxScheduleLatenessMs { get; set; }
+
+ public DateTime LastStartUtc { get; set; }
+ public DateTime LastCompletionUtc { get; set; }
+ public DateTime LastSuccessUtc { get; set; }
+ public DateTime LastFailureUtc { get; set; }
+ public DateTime LastTimeoutUtc { get; set; }
+ public double LastCompletionAgeMs { get; set; }
+
+ public AsyncTaskTimerTickResult LastResult { get; set; }
+ public string LastError { get; set; }
+ public bool Healthy { get; set; }
+ }
}
diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs
index 7da2f37c7..665a6b611 100644
--- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs
+++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs
@@ -2,23 +2,24 @@
using AgroBase.Forms.Operacoes;
using AgroBase.Models.Modules;
using AgroBase.Models.Operacoes;
+using AgroBase.Models.Operadores;
using AgroBase.Services;
+using AgroBase.Services.Operadores;
+using AgroMonitor;
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using static AgroBase.Models.Enums;
-using System.Drawing;
-using System.Text;
using static AgroBase.Services.OIDCanService.OidHandler;
-using AgroBase.Models.Operadores;
-using AgroBase.Services.Operadores;
-using AgroMonitor;
-using Newtonsoft.Json.Linq;
-using System.Globalization;
namespace AgroBase.Models
{
@@ -183,6 +184,9 @@ namespace AgroBase.Models
private static AsyncTaskTimerModel tmrLeituras;
private static AsyncTaskTimerModel tmrComunicacao;
+ private static string _discoverySessionId = Guid.NewGuid().ToString("N");
+ private static long _telemetrySequence = 0;
+
#region PARAMETRIZACAO
@@ -1694,269 +1698,1192 @@ namespace AgroBase.Models
}
}
- private void EnviarParametorsOperacao()
- {
- var topico = Variaveis.MqttServiceBase.Topicos.FirstOrDefault(x => x.Topico == VariaveisEquipamento.TopicoMqttParametros.Replace("", VariaveisEquipamento.Parametros.serial_number));
- var p = Variaveis.OperacaoEmAndamento.Parametros;
- string msg = JsonConvert.SerializeObject(p);
- Task.Run(async () => await Variaveis.MqttServiceBase.PublishAsync(topico, msg));
- }
-
- public void ExecutaComandoDaBase(OperacaoComandoBaseModel controleBase)
- {
- if (controleBase.Dispositivo == T_Code.Mod)
- {
- var _Parametros = controleBase.Parametros;
-
-
- switch (controleBase.Tecla)
- {
- case BotoesJoystick.Share: // Requisitar parametros da operacao
- EnviarParametorsOperacao();
- break;
- case BotoesJoystick.Options: // Operacao nao iniciada, carrega todos os parametros compeltos
- Variaveis.OperacaoEmAndamento.CarregarParamerosOperacaoBase(_Parametros);
- EnviarParametorsOperacao();
- break;
- case BotoesJoystick.Touchpad: // Operacao em andamento, atualiza apenas os dados de controle
- if (_Parametros.Modo != null) Variaveis.OperacaoEmAndamento.Parametros.Modo = _Parametros.Modo;
- if (_Parametros.QtdBicos != null) Variaveis.OperacaoEmAndamento.Parametros.QtdBicos = _Parametros.QtdBicos;
- if (_Parametros.Descricao != null) Variaveis.OperacaoEmAndamento.Parametros.Descricao = _Parametros.Descricao;
- if (_Parametros.QtdCamerasSolo != null) Variaveis.OperacaoEmAndamento.Parametros.QtdCamerasSolo = _Parametros.QtdCamerasSolo;
- if (_Parametros.CapacidadeReservatorio != null) Variaveis.OperacaoEmAndamento.Parametros.CapacidadeReservatorio = _Parametros.CapacidadeReservatorio;
-
- if (_Parametros.Controle != null) Variaveis.OperacaoEmAndamento.Parametros.Controle = _Parametros.Controle;
- if (_Parametros.ParametrosMandatorios != null) Variaveis.OperacaoEmAndamento.Parametros.ParametrosMandatorios = _Parametros.ParametrosMandatorios;
- if (_Parametros.ModulosMandatorios != null)
- {
- Variaveis.OperacaoEmAndamento.Parametros.ModulosMandatorios = _Parametros.ModulosMandatorios;
- Variaveis.OperacaoEmAndamento.DefinirComponentesEmUso();
- }
-
- HealthWorkerService.AtualizarDadosOperacao(true);
- EnviarParametorsOperacao();
- break;
- case BotoesJoystick.L1: // Inicia o modo simulador
- if (!Variaveis.OperacaoEmAndamento.Iniciado)
- Task.Run(async () => await Variaveis.OperacaoEmAndamento.IniciarSimulacao());
- break;
- case BotoesJoystick.L2: // Inicia a operacao
- if (!Variaveis.OperacaoEmAndamento.Iniciado)
- Task.Run(async () => await Variaveis.OperacaoEmAndamento.IniciarOperacao());
- break;
- case BotoesJoystick.L3: // Finaliza a operacao
- if (Variaveis.OperacaoEmAndamento.Iniciado)
- Task.Run(async () => await Variaveis.OperacaoEmAndamento.FinalizarOperacao());
- else
- ReiniciarOperacao(true);
- break;
- case BotoesJoystick.R1: // Libera a operacao interrompida mediante supervisao humana
- if (!(Variaveis.OperacaoEmAndamento.Trajetoria?.AutonomiaCorredor?.Liberado ?? false))
- {
- var (idx_corredor, b_liberada, h_liberado) = ((JObject)controleBase._comp_value).ToObject<(int, bool, bool)>();
- Variaveis.OperacaoEmAndamento.Trajetoria?.AtualizarDadosAutonomiaCorredor(idxCorredor: idx_corredor, bat_liberada: b_liberada, herb_liberado: h_liberado);
- }
- break;
- }
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Ipb)
- {
- if (controleBase._comp_value == true)
- {
- Variaveis.IniciarUDP();
- }
- else
- {
- Variaveis.StopUdpChannel();
- }
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Gps)
- {
- var (la_lateral, la_frontal) = ((JObject)controleBase._comp_value).ToObject<(double?, double?)>();
- if (la_lateral != null || la_frontal != null)
- GPSService.LeverArm = new GeoLeverArm(offsetFisicoFrontalCm: VariaveisEquipamento.LeverArmFrontalCm, offsetFisicoLateralCm: VariaveisEquipamento.LeverArmLateralCm, offsetCampoFrontalCm: la_frontal ?? 0, offsetCampoLateralCm: la_lateral ?? 0);
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Snr && Variaveis.OperacaoEmAndamento.DispSen?.Dados?.CameraCaminho?.Id != null)
- {
- RedisService.AtualizarCampos(
- RedisService.CamKey(Variaveis.OperacaoEmAndamento.DispSen.Dados.CameraCaminho.Id),
- ("streaming", controleBase.Tecla == BotoesJoystick.L1),
- ("frame_type", (TipoFrameCamera)controleBase._comp_value)
- );
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Cam && (Variaveis.OperacaoEmAndamento.DispSen?.Dados?.CamerasSolo?.Any(x => !string.IsNullOrEmpty(x.Id)) ?? false))
- {
- RedisService.AtualizarCampos(
- RedisService.CamKey(Variaveis.OperacaoEmAndamento.DispSen.Dados.CamerasSolo.FirstOrDefault(x => !string.IsNullOrEmpty(x.Id)).Id),
- ("streaming", controleBase.Tecla == BotoesJoystick.L1),
- ("frame_type", (TipoFrameCamera)controleBase._comp_value)
- );
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Atu && Variaveis.OperacaoEmAndamento.DispAtu != null)
- {
- if (controleBase.Controle == null) return;
-
- S_Code get_comp(string id)
- {
- if (string.IsNullOrEmpty(id)) return S_Code.sMOD;
- var d_atu = Variaveis.OperacaoEmAndamento.DispAtu.Dados;
- if (d_atu.BicosPulverizadores.Any(x => x.ID == id)) return S_Code.sBIC;
- if (d_atu.BombasPressurizadoras.Any(x => x.ID == id)) return S_Code.sBMB;
- if (d_atu.Sensores.Any(x => x.ID == id)) return d_atu.Sensores.First(x => x.ID == id).Componente;
- return S_Code.sVZO;
- }
-
- S_Code comp = get_comp(controleBase._comp_id);
- List valores = null;
-
- switch (comp)
- {
- case S_Code.sMOD:
- if (controleBase.Controle.Altura != null) Variaveis.OperacaoEmAndamento.Controle.AlturaBarra = (double)controleBase.Controle.Altura;
- break;
- case S_Code.sBIC:
- if (controleBase._comp_value != null)
- {
- var bico = Variaveis.OperacaoEmAndamento.Controle.Bicos.FirstOrDefault(x => x.ID == controleBase._comp_id);
- if (bico != null)
- {
- bico.AnguloAbertura = (int)controleBase._comp_value;
- valores = new List() { controleBase.Controle.AnguloSP };
- }
- }
- break;
- case S_Code.sBMB:
- switch (controleBase.Controle.Comando)
- {
- case CanMessagePosicaoDados.Command1:
- valores = new List() { controleBase.Controle.Potencia, controleBase.Controle.Pressao };
- break;
- case CanMessagePosicaoDados.Command2:
- Variaveis.OperacaoEmAndamento.Parametros.Controle.AtuModoControle = (ModoControleBomba)controleBase._comp_value;
- valores = new List() { (int)controleBase._comp_value, controleBase.Controle.Potencia };
- break;
- }
- break;
- }
-
- GeneralJoystick.EnviarComandoAtuador(comp, controleBase._comp_id, Status: controleBase.Controle.Estado, Valores: valores, Comando: controleBase.Controle.Comando, Forcar: true);
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Sen && Variaveis.OperacaoEmAndamento.DispSen != null)
- {
- S_Code get_comp(string id)
- {
- if (string.IsNullOrEmpty(id)) return S_Code.sMOD;
- var d_sen = Variaveis.OperacaoEmAndamento.DispSen.Dados;
- if (d_sen.Reles.Any(x => x.ID == id)) return S_Code.sRLE;
- if (d_sen.Servos.Any(x => x.ID == id)) return S_Code.sSRV;
- if (d_sen.Sinaleiros.Any(x => x.ID == id)) return S_Code.sLED;
- if (d_sen.Sensores.Any(x => x.ID == id)) return d_sen.Sensores.First(x => x.ID == id).Componente;
- return S_Code.sVZO;
- }
-
- S_Code comp = get_comp(controleBase._comp_id);
- GeneralJoystick.EnviarComandoSensoriamento(comp, controleBase._comp_id, controleBase._comp_value);
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Dir && controleBase.Tecla == BotoesJoystick.R3)
- {
- //GeneralJoystick.EnviaComandoMotor(Keys.R, ForcarComando: true, ID: controleBase._comp_id);
- GeneralJoystick.ProcessarDadosControle(BotoesJoystick.R3, false, ID: controleBase._comp_id, ForcarComando: true);
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Npc)
- {
- var (modo, e_entrada, e_saida, t_ligar, t_turbo) = ((JObject)controleBase._comp_value).ToObject<(CoolerControlService.CoolerMode, Estado?, Estado?, double?, double?)>();
- Variaveis.OperacaoEmAndamento.DispSen?.Dados?.CoolerControl?.AlterarControle(modo, entrada: e_entrada, saida: e_saida, tempOn: t_ligar, tempTurbo: t_turbo);
- return;
- }
-
- if (controleBase.Dispositivo == T_Code.Trj)
- {
- var (lat, lon, pontosRetorno) = ((JObject)controleBase._comp_value).ToObject<(double?, double?, List)>();
- GPSModel posicaoBase = null;
- if (lat != null && lon != null)
- posicaoBase = new GPSModel() { Latitude = lat.Value, Longitude = lon.Value };
- List pontosRetornoPos = null;
- if (pontosRetorno?.Any() ?? false)
- pontosRetornoPos = pontosRetorno.Select(x => new GPSModel() { Latitude = x[0], Longitude = x[1] }).ToList();
- Variaveis.OperacaoEmAndamento.Trajetoria?.IniciarRetornoBase(posicaoBase, pontosRetornoPos);
- return;
- }
-
- Variaveis.OperacaoEmAndamento.Emergencia = controleBase.Emergencia;
- if (Variaveis.OperacaoEmAndamento.Emergencia) return;
-
- Variaveis.OperacaoEmAndamento.Pausa = controleBase.Pausa;
- if (Variaveis.OperacaoEmAndamento.Pausa) return;
-
- if (controleBase.Controle == null) return;
-
- var cmdRover = GeneralJoystick.DeParaComandos.FirstOrDefault(x => x.botaoJoy == controleBase.Tecla);
-
- if (cmdRover?.Dispositivo == T_Code.Mov && (Variaveis.OperacaoEmAndamento.Parametros?.Controle?.MovimentoAutomatico ?? false) && Variaveis.OperacaoEmAndamento.Iniciado) return;
- if (cmdRover?.Dispositivo == T_Code.Dir && (Variaveis.OperacaoEmAndamento.Parametros?.Controle?.DirecionalAutomatico ?? false) && Variaveis.OperacaoEmAndamento.Iniciado) return;
-
- var _Controle = Variaveis.OperacaoEmAndamento.Controle;
- if (controleBase.Controle.PercentualVelocidadeSP != null) _Controle.PercentualVelocidadeSP = (double)controleBase.Controle.PercentualVelocidadeSP;
- if (controleBase.Controle.AnguloSP != null) _Controle.Angulo = (double)controleBase.Controle.AnguloSP;
- if (controleBase.Controle.TipoMovimentoDirecional != null) _Controle.TipoMovimento = (TipoMovimentoDirecional)controleBase.Controle.TipoMovimentoDirecional;
- if (controleBase.Controle.EmFreio != null) _Controle.EmFreio = (bool)controleBase.Controle.EmFreio;
-
- //GeneralJoystick.EnviaComandoMotor(tecla, controleBase.Dispositivo, ForcarComando: true, ID: controleBase._comp_id);
-
- switch (cmdRover?.Dispositivo)
- {
- case T_Code.Mov:
- GeneralJoystick.ProcessarDadosControle(cmdRover.botaoJoy, controleBase.Solto, valorDesejado: _Controle.PercentualVelocidadeSP, ForcarComando: true);
- break;
- case T_Code.Dir:
- GeneralJoystick.ProcessarDadosControle(cmdRover.botaoJoy, controleBase.Solto, valorDesejado: _Controle.Angulo, ForcarComando: true);
- break;
- }
- }
+
private async Task tmrComunicacao_Tick()
{
- if (Variaveis.MqttServiceBase == null) return;
+ var service = Variaveis.MqttServiceBase;
+
+ if (service == null)
+ return;
+
+ bool baseEncontrada =
+ (DateTime.Now - VariaveisOperacao
+ .PosicaoBase
+ .UltimoComandoRespondido)
+ .TotalSeconds < 10;
- bool base_encontrada = (DateTime.Now - VariaveisOperacao.PosicaoBase.UltimoComandoRespondido).TotalSeconds < 10;
MqttService.MqttTopicosModel topico = null;
- string msg = string.Empty;
- if (!base_encontrada)
+
+ if (!baseEncontrada)
{
- topico = Variaveis.MqttServiceBase.Topicos.FirstOrDefault(x => x.Topico == VariaveisMonitoramento.TopicoMqttDispositivos);
- msg = $"{VariaveisEquipamento.Parametros.serial_number},{VariaveisEquipamento.Parametros.rover_ip}";
+ topico = service.Topicos.FirstOrDefault(
+ x => x.Topico ==
+ VariaveisMonitoramento.TopicoMqttDispositivos
+ );
+
+ if (topico == null)
+ return;
+
+ var discovery = new
+ {
+ rover_id =
+ VariaveisEquipamento.Parametros.serial_number,
+ rover_ip =
+ VariaveisEquipamento.Parametros.rover_ip,
+ session_id =
+ _discoverySessionId,
+ sent_at_utc =
+ DateTime.UtcNow
+ };
+
+ string msg = JsonConvert.SerializeObject(discovery);
+
+ await service.PublishWithResultAsync(
+ topico,
+ msg
+ );
+
+ return;
}
- else
+
+ topico = service.Topicos.FirstOrDefault(
+ x => x.Topico ==
+ VariaveisEquipamento
+ .TopicoMqttTelemetria
+ .Replace(
+ "",
+ VariaveisEquipamento
+ .Parametros
+ .serial_number
+ )
+ );
+
+ if (topico == null)
+ return;
+
+ var dados =
+ Variaveis
+ .OperacaoEmAndamento
+ .Parametros
+ .DadosLeitura;
+
+ if (dados == null)
+ return;
+
+ dados.Atualizar();
+
+ OperacaoParametrosDadosModel snapshot =
+ dados.Clone();
+
+ long sequence = Interlocked.Increment(
+ ref _telemetrySequence
+ );
+
+ /*
+ * Por enquanto mantemos o payload como OperacaoParametrosDadosModel
+ * puro para compatibilidade com a base. O sequence já fica calculado
+ * aqui para a próxima etapa, quando entrarmos no TelemetryEnvelope.
+ */
+
+ string payload =
+ JsonConvert.SerializeObject(snapshot);
+
+ MqttPublishResult result =
+ await service.PublishWithResultAsync(
+ topico,
+ payload
+ );
+
+ if (result.Succeeded)
{
- topico = Variaveis.MqttServiceBase.Topicos.FirstOrDefault(x => x.Topico == VariaveisEquipamento.TopicoMqttTelemetria.Replace("", VariaveisEquipamento.Parametros.serial_number));
- Variaveis.OperacaoEmAndamento.Parametros.DadosLeitura?.Atualizar();
- var data = Variaveis.OperacaoEmAndamento.Parametros.DadosLeitura?.Clone();
- msg = JsonConvert.SerializeObject(data);
- }
- if (topico != null && !string.IsNullOrEmpty(msg))
- {
- await Variaveis.MqttServiceBase.PublishAsync(topico, msg);
- if (Variaveis.OperacaoEmAndamento.Parametros.DadosLeitura != null)
- Variaveis.OperacaoEmAndamento.Parametros.DadosLeitura.UltimoEnvioLog = DateTime.Now;
+ /*
+ * O cursor dos logs só avança se a telemetria foi aceita
+ * pelo broker. Assim, se o publish falhar, os logs entram
+ * novamente no próximo snapshot.
+ */
+ dados.UltimoEnvioLog = snapshot.Momento;
}
}
+
+
+ // ============================================================================
+ // CAMPOS
+ // ============================================================================
+
+ private readonly SemaphoreSlim _baseCommandGate =
+ new SemaphoreSlim(1, 1);
+
+ private readonly object _baseCommandDedupeLock =
+ new object();
+
+ private readonly Dictionary _baseCommandDedupe =
+ new Dictionary();
+
+ private static readonly TimeSpan BaseCommandDedupeWindow =
+ TimeSpan.FromMilliseconds(1500);
+
+
+ // ============================================================================
+ // PUBLICAÇÃO DE PARÂMETROS
+ // ============================================================================
+
+ private void EnviarParametorsOperacao()
+ {
+ Forget(
+ EnviarParametrosOperacaoAsync(),
+ "Enviar parâmetros da operação"
+ );
+ }
+
+ private async Task EnviarParametrosOperacaoAsync()
+ {
+ var service = Variaveis.MqttServiceBase;
+
+ if (service == null)
+ return;
+
+ string topicName =
+ VariaveisEquipamento
+ .TopicoMqttParametros
+ .Replace(
+ "",
+ VariaveisEquipamento
+ .Parametros
+ .serial_number
+ );
+
+ var topico = service.Topicos
+ .FirstOrDefault(x => x.Topico == topicName);
+
+ if (topico == null)
+ return;
+
+ var parametros =
+ Variaveis
+ .OperacaoEmAndamento?
+ .Parametros;
+
+ if (parametros == null)
+ return;
+
+ string msg =
+ JsonConvert.SerializeObject(parametros);
+
+ /*
+ * Preferencial com o MqttService novo.
+ * Se sua versão ainda não tiver PublishWithResultAsync,
+ * troque este bloco por:
+ *
+ * await service.PublishAsync(topico, msg);
+ */
+ MqttPublishResult result =
+ await service.PublishWithResultAsync(
+ topico,
+ msg
+ ).ConfigureAwait(false);
+
+ if (!result.Succeeded)
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Falha ao publicar parâmetros: " +
+ result.Status + " " + result.Error
+ );
+ }
+ }
+
+
+ // ============================================================================
+ // ENTRADA COMPATÍVEL
+ // ============================================================================
+
+ public void ExecutaComandoDaBase(
+ OperacaoComandoBaseModel controleBase)
+ {
+ Forget(
+ ExecutaComandoDaBaseAsync(controleBase),
+ "Executar comando da base"
+ );
+ }
+
+ public async Task ExecutaComandoDaBaseAsync(
+ OperacaoComandoBaseModel controleBase)
+ {
+ if (controleBase == null)
+ return;
+
+ if (Variaveis.Fechando)
+ return;
+
+ if (EhComandoDuplicado(controleBase))
+ return;
+
+ bool entered = false;
+
+ try
+ {
+ /*
+ * Comando remoto é rota crítica.
+ * Processar em série evita:
+ * - iniciar e finalizar operação ao mesmo tempo;
+ * - atualizar parâmetros enquanto inicia operação;
+ * - dois comandos de atuador disputando o mesmo estado local.
+ */
+ await _baseCommandGate
+ .WaitAsync()
+ .ConfigureAwait(false);
+
+ entered = true;
+
+ await ExecutaComandoDaBaseInternoAsync(
+ controleBase
+ ).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Erro ao executar comando: " +
+ ex
+ );
+ }
+ finally
+ {
+ if (entered)
+ _baseCommandGate.Release();
+ }
+ }
+
+ private async Task ExecutaComandoDaBaseInternoAsync(
+ OperacaoComandoBaseModel controleBase)
+ {
+ var op = Variaveis.OperacaoEmAndamento;
+
+ if (op == null)
+ return;
+
+ switch (controleBase.Dispositivo)
+ {
+ case T_Code.Mod:
+ await ExecutarComandoModuloAsync(
+ controleBase,
+ op
+ ).ConfigureAwait(false);
+ return;
+
+ case T_Code.Ipb:
+ ExecutarComandoIpBridge(controleBase);
+ return;
+
+ case T_Code.Gps:
+ ExecutarComandoGps(controleBase);
+ return;
+
+ case T_Code.Snr:
+ ExecutarComandoCameraCaminho(controleBase, op);
+ return;
+
+ case T_Code.Cam:
+ ExecutarComandoCameraSolo(controleBase, op);
+ return;
+
+ case T_Code.Atu:
+ ExecutarComandoAtuador(controleBase, op);
+ return;
+
+ case T_Code.Sen:
+ ExecutarComandoSensoriamento(controleBase, op);
+ return;
+
+ case T_Code.Dir:
+ if (controleBase.Tecla == BotoesJoystick.R3)
+ {
+ GeneralJoystick.ProcessarDadosControle(
+ BotoesJoystick.R3,
+ false,
+ ID: controleBase._comp_id,
+ ForcarComando: true
+ );
+ return;
+ }
+ break;
+
+ case T_Code.Npc:
+ ExecutarComandoNpc(controleBase, op);
+ return;
+
+ case T_Code.Trj:
+ ExecutarComandoTrajetoria(controleBase, op);
+ return;
+ }
+
+ ExecutarComandoMovDirGenerico(controleBase, op);
+ }
+
+
+ // ============================================================================
+ // COMANDOS DE OPERAÇÃO
+ // ============================================================================
+
+ private async Task ExecutarComandoModuloAsync(
+ OperacaoComandoBaseModel controleBase,
+ dynamic op)
+ {
+ var parametros = controleBase.Parametros;
+
+ switch (controleBase.Tecla)
+ {
+ case BotoesJoystick.Share:
+ await EnviarParametrosOperacaoAsync()
+ .ConfigureAwait(false);
+ return;
+
+ case BotoesJoystick.Options:
+ if (parametros == null)
+ return;
+
+ /*
+ * Carga completa de parâmetros. Idealmente só antes
+ * da operação estar iniciada.
+ */
+ if (op.Iniciado)
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Ignorando carga completa de parâmetros com operação iniciada."
+ );
+
+ return;
+ }
+
+ op.CarregarParamerosOperacaoBase(parametros);
+
+ await EnviarParametrosOperacaoAsync()
+ .ConfigureAwait(false);
+ return;
+
+ case BotoesJoystick.Touchpad:
+ if (parametros == null)
+ return;
+
+ AtualizarParametrosParciais(parametros, op);
+
+ HealthWorkerService.AtualizarDadosOperacao(true);
+
+ await EnviarParametrosOperacaoAsync()
+ .ConfigureAwait(false);
+ return;
+
+ case BotoesJoystick.L1:
+ if (!op.Iniciado)
+ await op.IniciarSimulacao()
+ .ConfigureAwait(false);
+ return;
+
+ case BotoesJoystick.L2:
+ if (!op.Iniciado)
+ await op.IniciarOperacao()
+ .ConfigureAwait(false);
+ return;
+
+ case BotoesJoystick.L3:
+ if (op.Iniciado)
+ await op.FinalizarOperacao()
+ .ConfigureAwait(false);
+ else
+ ReiniciarOperacao(true);
+ return;
+
+ case BotoesJoystick.R1:
+ ExecutarLiberacaoHumanaAutonomia(
+ controleBase,
+ op
+ );
+ return;
+ }
+ }
+
+ private static void AtualizarParametrosParciais(
+ OperacaoParametrosModel parametros,
+ dynamic op)
+ {
+ if (op?.Parametros == null)
+ return;
+
+ if (parametros.Modo != null)
+ op.Parametros.Modo = parametros.Modo;
+
+ if (parametros.QtdBicos != null)
+ op.Parametros.QtdBicos = parametros.QtdBicos;
+
+ if (parametros.Descricao != null)
+ op.Parametros.Descricao = parametros.Descricao;
+
+ if (parametros.QtdCamerasSolo != null)
+ op.Parametros.QtdCamerasSolo =
+ parametros.QtdCamerasSolo;
+
+ if (parametros.CapacidadeReservatorio != null)
+ op.Parametros.CapacidadeReservatorio =
+ parametros.CapacidadeReservatorio;
+
+ if (parametros.Controle != null)
+ op.Parametros.Controle = parametros.Controle;
+
+ if (parametros.ParametrosMandatorios != null)
+ op.Parametros.ParametrosMandatorios =
+ parametros.ParametrosMandatorios;
+
+ if (parametros.ModulosMandatorios != null)
+ {
+ op.Parametros.ModulosMandatorios =
+ parametros.ModulosMandatorios;
+
+ op.DefinirComponentesEmUso();
+ }
+ }
+
+ private void ExecutarLiberacaoHumanaAutonomia(
+ OperacaoComandoBaseModel controleBase,
+ dynamic op)
+ {
+ if (op?.Trajetoria?.AutonomiaCorredor?.Liberado ?? false)
+ return;
+
+ if (!TryDeserializeValue(
+ controleBase._comp_value,
+ out ValueTuple data))
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Payload inválido para liberação humana."
+ );
+ return;
+ }
+
+ op.Trajetoria?
+ .AtualizarDadosAutonomiaCorredor(
+ idxCorredor: data.Item1,
+ bat_liberada: data.Item2,
+ herb_liberado: data.Item3
+ );
+ }
+
+
+ // ============================================================================
+ // COMANDOS INDIVIDUAIS
+ // ============================================================================
+
+ private void ExecutarComandoIpBridge(
+ OperacaoComandoBaseModel controleBase)
+ {
+ bool ligar;
+
+ if (!TryDeserializeValue(
+ controleBase._comp_value,
+ out ligar))
+ {
+ return;
+ }
+
+ if (ligar)
+ Variaveis.IniciarUDP();
+ else
+ Variaveis.StopUdpChannel();
+ }
+
+ private void ExecutarComandoGps(
+ OperacaoComandoBaseModel controleBase)
+ {
+ if (!TryDeserializeValue(
+ controleBase._comp_value,
+ out ValueTuple lever))
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Payload inválido para lever arm."
+ );
+ return;
+ }
+
+ double? laLateral = lever.Item1;
+ double? laFrontal = lever.Item2;
+
+ if (laLateral == null && laFrontal == null)
+ return;
+
+ GPSService.LeverArm = new GeoLeverArm(
+ offsetFisicoFrontalCm:
+ VariaveisEquipamento.LeverArmFrontalCm,
+ offsetFisicoLateralCm:
+ VariaveisEquipamento.LeverArmLateralCm,
+ offsetCampoFrontalCm:
+ laFrontal ?? 0,
+ offsetCampoLateralCm:
+ laLateral ?? 0
+ );
+ }
+
+ private void ExecutarComandoCameraCaminho(
+ OperacaoComandoBaseModel controleBase,
+ dynamic op)
+ {
+ string cameraId =
+ op?.DispSen?.Dados?.CameraCaminho?.Id;
+
+ if (string.IsNullOrWhiteSpace(cameraId))
+ return;
+
+ if (!TryDeserializeValue(
+ controleBase._comp_value,
+ out TipoFrameCamera frameType))
+ {
+ return;
+ }
+
+ RedisService.AtualizarCampos(
+ RedisService.CamKey(cameraId),
+ ("streaming", controleBase.Tecla == BotoesJoystick.L1),
+ ("frame_type", frameType)
+ );
+ }
+
+ private void ExecutarComandoCameraSolo(
+ OperacaoComandoBaseModel controleBase,
+ dynamic op)
+ {
+ var cameras =
+ op?.DispSen?.Dados?.CamerasSolo;
+
+ string cameraId = null;
+
+ if (cameras != null)
+ {
+ foreach (var camera in cameras)
+ {
+ if (!string.IsNullOrWhiteSpace(camera.Id))
+ {
+ cameraId = camera.Id;
+ break;
+ }
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(cameraId))
+ return;
+
+ if (!TryDeserializeValue(
+ controleBase._comp_value,
+ out TipoFrameCamera frameType))
+ {
+ return;
+ }
+
+ RedisService.AtualizarCampos(
+ RedisService.CamKey(cameraId),
+ ("streaming", controleBase.Tecla == BotoesJoystick.L1),
+ ("frame_type", frameType)
+ );
+ }
+
+ private void ExecutarComandoAtuador(
+ OperacaoComandoBaseModel controleBase,
+ OperacaoModel op)
+ {
+ if (controleBase.Controle == null)
+ return;
+
+ var dadosAtu = op?.DispAtu?.Dados;
+
+ if (dadosAtu == null)
+ return;
+
+ S_Code comp =
+ ResolverComponenteAtuador(
+ dadosAtu,
+ controleBase._comp_id
+ );
+
+ if (comp == S_Code.sVZO)
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Componente ATU não encontrado: " +
+ controleBase._comp_id
+ );
+ return;
+ }
+
+ List valores = null;
+
+ switch (comp)
+ {
+ case S_Code.sMOD:
+ if (controleBase.Controle.Altura != null)
+ {
+ op.Controle.AlturaBarra =
+ (double)controleBase.Controle.Altura;
+ }
+ break;
+
+ case S_Code.sBIC:
+ if (controleBase._comp_value != null)
+ {
+ int angulo;
+
+ if (TryDeserializeValue(
+ controleBase._comp_value,
+ out angulo))
+ {
+ var bico =
+ op.Controle
+ .Bicos
+ .FirstOrDefault(
+ x => x.ID ==
+ controleBase._comp_id
+ );
+
+ if (bico != null)
+ bico.AnguloAbertura = angulo;
+ }
+
+ valores = new List
+ {
+ controleBase.Controle.AnguloSP
+ };
+ }
+ break;
+
+ case S_Code.sBMB:
+ switch (controleBase.Controle.Comando)
+ {
+ case CanMessagePosicaoDados.Command1:
+ valores = new List
+ {
+ controleBase.Controle.Potencia,
+ controleBase.Controle.Pressao
+ };
+ break;
+
+ case CanMessagePosicaoDados.Command2:
+ ModoControleBomba modo;
+
+ if (TryDeserializeValue(
+ controleBase._comp_value,
+ out modo))
+ {
+ if (op.Parametros?.Controle != null)
+ {
+ op.Parametros
+ .Controle
+ .AtuModoControle = modo;
+ }
+
+ valores = new List
+ {
+ (int)modo,
+ controleBase.Controle.Potencia
+ };
+ }
+ break;
+ }
+ break;
+ }
+
+ GeneralJoystick.EnviarComandoAtuador(
+ comp,
+ controleBase._comp_id,
+ Status: controleBase.Controle.Estado,
+ Valores: valores,
+ Comando: controleBase.Controle.Comando,
+ Forcar: true
+ );
+ }
+
+ private void ExecutarComandoSensoriamento(
+ OperacaoComandoBaseModel controleBase,
+ dynamic op)
+ {
+ var dadosSen = op?.DispSen?.Dados;
+
+ if (dadosSen == null)
+ return;
+
+ S_Code comp =
+ ResolverComponenteSensoriamento(
+ dadosSen,
+ controleBase._comp_id
+ );
+
+ if (comp == S_Code.sVZO)
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Componente SEN não encontrado: " +
+ controleBase._comp_id
+ );
+ return;
+ }
+
+ GeneralJoystick.EnviarComandoSensoriamento(
+ comp,
+ controleBase._comp_id,
+ controleBase._comp_value
+ );
+ }
+
+ private void ExecutarComandoNpc(
+ OperacaoComandoBaseModel controleBase,
+ dynamic op)
+ {
+ if (!TryDeserializeValue(
+ controleBase._comp_value,
+ out ValueTuple<
+ CoolerControlService.CoolerMode,
+ Estado?,
+ Estado?,
+ double?,
+ double?> data))
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Payload inválido para cooler."
+ );
+ return;
+ }
+
+ op?.DispSen?
+ .Dados?
+ .CoolerControl?
+ .AlterarControle(
+ data.Item1,
+ entrada: data.Item2,
+ saida: data.Item3,
+ tempOn: data.Item4,
+ tempTurbo: data.Item5
+ );
+ }
+
+ private void ExecutarComandoTrajetoria(
+ OperacaoComandoBaseModel controleBase,
+ dynamic op)
+ {
+ if (!TryDeserializeValue(
+ controleBase._comp_value,
+ out ValueTuple<
+ double?,
+ double?,
+ List> data))
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Payload inválido para retorno base."
+ );
+ return;
+ }
+
+ GPSModel posicaoBase = null;
+
+ if (data.Item1 != null && data.Item2 != null)
+ {
+ posicaoBase = new GPSModel
+ {
+ Latitude = data.Item1.Value,
+ Longitude = data.Item2.Value
+ };
+ }
+
+ List pontosRetornoPos = null;
+
+ if (data.Item3 != null && data.Item3.Any())
+ {
+ pontosRetornoPos = data.Item3
+ .Where(x => x != null && x.Length >= 2)
+ .Select(x => new GPSModel
+ {
+ Latitude = x[0],
+ Longitude = x[1]
+ })
+ .ToList();
+ }
+
+ op?.Trajetoria?
+ .IniciarRetornoBase(
+ posicaoBase,
+ pontosRetornoPos
+ );
+ }
+
+
+ // ============================================================================
+ // MOV/DIR GENÉRICO
+ // ============================================================================
+
+ private void ExecutarComandoMovDirGenerico(
+ OperacaoComandoBaseModel controleBase,
+ dynamic op)
+ {
+ op.Emergencia = controleBase.Emergencia;
+
+ if (op.Emergencia)
+ return;
+
+ op.Pausa = controleBase.Pausa;
+
+ if (op.Pausa)
+ return;
+
+ if (controleBase.Controle == null)
+ return;
+
+ var cmdRover = GeneralJoystick
+ .DeParaComandos
+ .FirstOrDefault(
+ x => x.botaoJoy == controleBase.Tecla
+ );
+
+ if (cmdRover == null)
+ return;
+
+ if (cmdRover.Dispositivo == T_Code.Mov &&
+ (op.Parametros?.Controle?.MovimentoAutomatico ?? false) &&
+ op.Iniciado)
+ {
+ return;
+ }
+
+ if (cmdRover.Dispositivo == T_Code.Dir &&
+ (op.Parametros?.Controle?.DirecionalAutomatico ?? false) &&
+ op.Iniciado)
+ {
+ return;
+ }
+
+ var controle = op.Controle;
+
+ if (controle == null)
+ return;
+
+ if (controleBase.Controle.PercentualVelocidadeSP != null)
+ controle.PercentualVelocidadeSP =
+ (double)controleBase
+ .Controle
+ .PercentualVelocidadeSP;
+
+ if (controleBase.Controle.AnguloSP != null)
+ controle.Angulo =
+ (double)controleBase
+ .Controle
+ .AnguloSP;
+
+ if (controleBase.Controle.TipoMovimentoDirecional != null)
+ controle.TipoMovimento =
+ (TipoMovimentoDirecional)controleBase
+ .Controle
+ .TipoMovimentoDirecional;
+
+ if (controleBase.Controle.EmFreio != null)
+ controle.EmFreio =
+ (bool)controleBase
+ .Controle
+ .EmFreio;
+
+ switch (cmdRover.Dispositivo)
+ {
+ case T_Code.Mov:
+ GeneralJoystick.ProcessarDadosControle(
+ cmdRover.botaoJoy,
+ controleBase.Solto,
+ valorDesejado:
+ controle.PercentualVelocidadeSP,
+ ForcarComando: true
+ );
+ break;
+
+ case T_Code.Dir:
+ GeneralJoystick.ProcessarDadosControle(
+ cmdRover.botaoJoy,
+ controleBase.Solto,
+ valorDesejado: controle.Angulo,
+ ForcarComando: true
+ );
+ break;
+ }
+ }
+
+
+ // ============================================================================
+ // RESOLUÇÃO DE COMPONENTES
+ // ============================================================================
+
+ private static S_Code ResolverComponenteAtuador(
+ dynamic dadosAtu,
+ string id)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ return S_Code.sMOD;
+
+ if (dadosAtu?.BicosPulverizadores != null)
+ {
+ foreach (var item in dadosAtu.BicosPulverizadores)
+ if (item.ID == id)
+ return S_Code.sBIC;
+ }
+
+ if (dadosAtu?.BombasPressurizadoras != null)
+ {
+ foreach (var item in dadosAtu.BombasPressurizadoras)
+ if (item.ID == id)
+ return S_Code.sBMB;
+ }
+
+ if (dadosAtu?.Sensores != null)
+ {
+ foreach (var item in dadosAtu.Sensores)
+ if (item.ID == id)
+ return item.Componente;
+ }
+
+ return S_Code.sVZO;
+ }
+
+ private static S_Code ResolverComponenteSensoriamento(
+ dynamic dadosSen,
+ string id)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ return S_Code.sMOD;
+
+ if (dadosSen?.Reles != null)
+ {
+ foreach (var item in dadosSen.Reles)
+ if (item.ID == id)
+ return S_Code.sRLE;
+ }
+
+ if (dadosSen?.Servos != null)
+ {
+ foreach (var item in dadosSen.Servos)
+ if (item.ID == id)
+ return S_Code.sSRV;
+ }
+
+ if (dadosSen?.Sinaleiros != null)
+ {
+ foreach (var item in dadosSen.Sinaleiros)
+ if (item.ID == id)
+ return S_Code.sLED;
+ }
+
+ if (dadosSen?.Sensores != null)
+ {
+ foreach (var item in dadosSen.Sensores)
+ if (item.ID == id)
+ return item.Componente;
+ }
+
+ return S_Code.sVZO;
+ }
+
+
+ // ============================================================================
+ // DEDUPE
+ // ============================================================================
+
+ private bool EhComandoDuplicado(
+ OperacaoComandoBaseModel comando)
+ {
+ if (!DeveDeduplicar(comando))
+ return false;
+
+ string key = CriarChaveDedupe(comando);
+
+ if (string.IsNullOrWhiteSpace(key))
+ return false;
+
+ DateTime now = DateTime.UtcNow;
+
+ lock (_baseCommandDedupeLock)
+ {
+ /*
+ * Limpeza barata.
+ */
+ var expirados = _baseCommandDedupe
+ .Where(x => now - x.Value >
+ TimeSpan.FromSeconds(5))
+ .Select(x => x.Key)
+ .ToList();
+
+ foreach (string item in expirados)
+ _baseCommandDedupe.Remove(item);
+
+ DateTime last;
+
+ if (_baseCommandDedupe.TryGetValue(key, out last) &&
+ now - last < BaseCommandDedupeWindow)
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] Comando duplicado ignorado: " +
+ comando.Dispositivo + "/" + comando.Tecla
+ );
+
+ return true;
+ }
+
+ _baseCommandDedupe[key] = now;
+ return false;
+ }
+ }
+
+ private static bool DeveDeduplicar(
+ OperacaoComandoBaseModel comando)
+ {
+ if (comando == null)
+ return false;
+
+ /*
+ * Não deduplicamos MOV/DIR genérico porque o controle manual
+ * pode repetir o mesmo valor para manter atuação/estado.
+ *
+ * Deduplicamos comandos de alto nível e configuração, que são
+ * perigosos se QoS1 entregar duplicado.
+ */
+ switch (comando.Dispositivo)
+ {
+ case T_Code.Mod:
+ case T_Code.Ipb:
+ case T_Code.Gps:
+ case T_Code.Snr:
+ case T_Code.Cam:
+ case T_Code.Npc:
+ case T_Code.Trj:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static string CriarChaveDedupe(
+ OperacaoComandoBaseModel comando)
+ {
+ try
+ {
+ return string.Join(
+ "|",
+ comando.Dispositivo,
+ comando.Tecla,
+ comando._comp_id ?? string.Empty,
+ comando.Solto,
+ comando.Emergencia,
+ comando.Pausa,
+ JsonConvert.SerializeObject(comando._comp_value),
+ JsonConvert.SerializeObject(comando.Parametros),
+ JsonConvert.SerializeObject(comando.Controle)
+ );
+ }
+ catch
+ {
+ return comando.Dispositivo + "|" +
+ comando.Tecla + "|" +
+ comando._comp_id;
+ }
+ }
+
+
+ // ============================================================================
+ // CONVERSÃO SEGURA DE PAYLOAD
+ // ============================================================================
+
+ private static bool TryDeserializeValue(
+ object value,
+ out T result)
+ {
+ result = default(T);
+
+ try
+ {
+ if (value == null)
+ return false;
+
+ if (value is T typed)
+ {
+ result = typed;
+ return true;
+ }
+
+ JToken token = value as JToken;
+
+ if (token != null)
+ {
+ result = token.ToObject();
+ return true;
+ }
+
+ string text = value as string;
+
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ result = JsonConvert.DeserializeObject(text);
+ return true;
+ }
+
+ Type target =
+ Nullable.GetUnderlyingType(typeof(T)) ??
+ typeof(T);
+
+ if (target.IsEnum)
+ {
+ object enumValue =
+ Enum.ToObject(target, value);
+
+ result = (T)enumValue;
+ return true;
+ }
+
+ result = (T)Convert.ChangeType(value, target);
+ return true;
+ }
+ catch
+ {
+ result = default(T);
+ return false;
+ }
+ }
+
+
+ // ============================================================================
+ // TASK FIRE-AND-FORGET OBSERVADO
+ // ============================================================================
+
+ private static void Forget(
+ Task task,
+ string context)
+ {
+ if (task == null)
+ return;
+
+ _ = task.ContinueWith(
+ completed =>
+ {
+ Exception ex =
+ completed.Exception?
+ .GetBaseException();
+
+ if (ex != null)
+ {
+ try
+ {
+ Variaveis.MostrarLog(
+ "[ComandoBase] " +
+ context + ": " +
+ ex.Message
+ );
+ }
+ catch { }
+ }
+ },
+ CancellationToken.None,
+ TaskContinuationOptions.OnlyOnFaulted |
+ TaskContinuationOptions.ExecuteSynchronously,
+ TaskScheduler.Default
+ );
+ }
+
+
+
#endregion
#region REGISTRO DE LOGS
diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoParametrosModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoParametrosModel.cs
index 83143958b..f441468e1 100644
--- a/AgroBase/AgroBase/Models/Operacoes/OperacaoParametrosModel.cs
+++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoParametrosModel.cs
@@ -150,8 +150,20 @@ namespace AgroBase.Models.Operacoes
public MapaFeatureCollectionModel Mapa { get; set; }
public List RuasPercorrer { get; set; }
public List PontosRetorno { get; set; }
-
- public OperacaoParametrosDadosModel DadosLeitura { get; set; }
+
+ private OperacaoParametrosDadosModel _dadosLeitura;
+ public OperacaoParametrosDadosModel DadosLeitura
+ {
+ get => _dadosLeitura;
+ set
+ {
+ if (!object.ReferenceEquals(_dadosLeitura, value))
+ {
+ _dadosLeitura = value;
+ OnPropertyChanged(nameof(DadosLeitura));
+ }
+ }
+ }
public event PropertyChangedEventHandler PropertyChanged;
@@ -561,75 +573,155 @@ namespace AgroBase.Models.Operacoes
public DateTime Momento { get; set; }
public StatusModulo StatusRover { get; set; }
public CanServiceTipo TipoCanAtivo { get; set; }
+
public OperacaoParametrosDadosOperacaoModel Operacao { get; set; }
public OperacaoParametrosDadosTrajetoriaModel Trajetoria { get; set; }
public OperacaoParametrosDadosControleModel Controle { get; set; }
+
public List ModulosSaude { get; set; }
public List DispositivosMapeados { get; set; }
+
public OperacaoSensoriamentoLogImuModel Imu { get; set; }
public OperacaoParametrosDadosAtuadorModel Atuador { get; set; }
public OperacaoParametrosDadosSensoriamentoModel Sensoriamento { get; set; }
public OperacaoParametrosDadosMovimentacaoModel Movimentacao { get; set; }
public OperacaoParametrosDadosDirecionalModel Direcional { get; set; }
+
public OperacaoSensoriamentoLogBateriaModel Bateria { get; set; }
public CoolerControlDataModel Refrigeracao { get; set; }
public OperacaoParametrosDadosGNSSModel Gnss { get; set; }
public PerformanceModel PerformanceNcp { get; set; }
public LivoxModel LivoxLidar { get; set; }
+
public DateTime UltimoEnvioLog { get; set; } = DateTime.MinValue;
+
public List Logs { get; set; }
public List Cameras { get; set; }
public VisualWorkerResumoModel OperadorVisual { get; set; }
public void Atualizar()
{
+ DateTime agora = DateTime.Now;
+
+ Momento = agora;
+
var op = Variaveis.OperacaoEmAndamento;
- var sen = op.Sensoriamento;
- var dAtu = op.DispAtu?.Dados;
- var dSen = op.DispSen?.Dados;
- var dMvd = op.DispMvd?.Dados;
+ var sen = op?.Sensoriamento;
- if (dAtu == null || dSen == null || dMvd == null) return;
+ var dAtu = op?.DispAtu?.Dados;
+ var dSen = op?.DispSen?.Dados;
+ var dMvd = op?.DispMvd?.Dados;
- Momento = DateTime.Now;
- StatusRover = op.Status;
+ StatusRover = op?.Status ?? StatusModulo.Desconectado;
TipoCanAtivo = CanManager.TipoServico;
- Operacao = new OperacaoParametrosDadosOperacaoModel()
+
+ AtualizarOperacao(op);
+ AtualizarTrajetoria(sen);
+ AtualizarControle(op);
+ AtualizarSaude(sen);
+ AtualizarImu(sen);
+ AtualizarAtuador(sen, dAtu);
+ AtualizarSensoriamento(dSen);
+ AtualizarMovimentacao(sen, dMvd);
+ AtualizarDirecional(sen, dMvd);
+ AtualizarGnss(sen);
+ AtualizarOutrosBlocos(sen);
+ AtualizarCameras();
+ AtualizarLogs(sen);
+ }
+
+ private void AtualizarOperacao(dynamic op)
+ {
+ var parametros = op?.Parametros;
+
+ Operacao = new OperacaoParametrosDadosOperacaoModel
{
- Iniciada = op.Iniciado,
- Liberada = op.OperacaoLiberada,
- Status = op.StatusAtual,
- Modo = op.Parametros?.Modo ?? ModoOperacao.NaoDefinido,
- ImpedimentoErro = op.ErroOperacaoLiberada,
- TempoAguardandoSegs = op.TempoAguardandoSegs,
- TempoDecorridoSegs = op.TempoDecorridoSegs,
- Emergencia = op.Emergencia,
- Pausa = op.Pausa,
- IniciadoEm = op.DataInicio,
- FinalizadoEm = op.DataFim,
- ModulosMandatorios = new List(op.Parametros.ModulosMandatorios ?? new List()),
- ParametrosMandatorios = new List(op.Parametros.ParametrosMandatorios ?? new List())
+ Iniciada = op?.Iniciado ?? false,
+ Liberada = op?.OperacaoLiberada ?? false,
+ Status = op?.StatusAtual ?? StatusOperacao.Erro,
+ Modo = parametros?.Modo ?? ModoOperacao.NaoDefinido,
+ ImpedimentoErro = op?.ErroOperacaoLiberada,
+ TempoAguardandoSegs = op?.TempoAguardandoSegs ?? 0,
+ TempoDecorridoSegs = op?.TempoDecorridoSegs ?? 0,
+ Emergencia = op?.Emergencia ?? false,
+ Pausa = op?.Pausa ?? false,
+ IniciadoEm = op?.DataInicio ?? DateTime.MinValue,
+ FinalizadoEm = op?.DataFim ?? DateTime.MinValue,
+ ModulosMandatorios = CloneList(
+ parametros?.ModulosMandatorios
+ ),
+ ParametrosMandatorios = CloneList(
+ parametros?.ParametrosMandatorios
+ )
};
+ }
+
+ private void AtualizarTrajetoria(dynamic sen)
+ {
var t = sen?.Trajetoria;
- Trajetoria = t != null ? new OperacaoParametrosDadosTrajetoriaModel()
+
+ if (t == null)
{
- AutonomiaCorredor = new OperacaoParametrosDadosTrajetoriaAutonomiaCorredorModel()
+ Trajetoria = new OperacaoParametrosDadosTrajetoriaModel
{
- BateriaSuficiente = t.AutonomiaCooredor.BateriaSuficiente,
- DistanciaCorredor_m = t.AutonomiaCooredor.DistanciaSeguraBateria_m,
- DistanciaSeguraBateria_m = t.AutonomiaCooredor.DistanciaSeguraBateria_m,
- DistanciaSeguraHerbicida_m = t.AutonomiaCooredor.DistanciaSeguraBateria_m,
- HerbicidaSuficiente = t.AutonomiaCooredor.HerbicidaSuficiente,
- Liberado = t.AutonomiaCooredor.Liberado,
- Motivo = t.AutonomiaCooredor.Motivo,
- Motivos = t.AutonomiaCooredor.Motivos.ToList(),
- Status = t.AutonomiaCooredor.Status
- },
+ AutonomiaCorredor =
+ new OperacaoParametrosDadosTrajetoriaAutonomiaCorredorModel()
+ };
+ return;
+ }
+
+ var autonomia = t.AutonomiaCooredor;
+ var corredorAtual = t.CorredorAtual;
+ var pontoAtual = t.PontoAtual;
+ var proximoPonto = t.ProximoPonto;
+
+ Trajetoria = new OperacaoParametrosDadosTrajetoriaModel
+ {
+ AutonomiaCorredor =
+ autonomia != null
+ ? new OperacaoParametrosDadosTrajetoriaAutonomiaCorredorModel
+ {
+ BateriaSuficiente = autonomia.BateriaSuficiente,
+ DistanciaCorredor_m = TryGetDouble(
+ autonomia,
+ "DistanciaCorredor_m",
+ TryGetDouble(
+ autonomia,
+ "DistanciaCorredor",
+ autonomia.DistanciaSeguraBateria_m
+ )
+ ),
+ DistanciaSeguraBateria_m =
+ autonomia.DistanciaSeguraBateria_m,
+ DistanciaSeguraHerbicida_m = TryGetDouble(
+ autonomia,
+ "DistanciaSeguraHerbicida_m",
+ TryGetDouble(
+ autonomia,
+ "DistanciaSeguraHerbicida",
+ autonomia.DistanciaSeguraBateria_m
+ )
+ ),
+ HerbicidaSuficiente =
+ autonomia.HerbicidaSuficiente,
+ Liberado = autonomia.Liberado,
+ Motivo = autonomia.Motivo,
+ Motivos = CloneListString(autonomia.Motivos),
+ Status = autonomia.Status
+ }
+ : new OperacaoParametrosDadosTrajetoriaAutonomiaCorredorModel(),
+
AnguloCaminho = t.AnguloCaminho,
- CorredorAtualDentro = t.CorredorAtual.Dentro,
- CorredorAtualDistanciaPercorrida = t.CorredorAtual.DistanciaPercorridaCorredor,
- CorredorAtualDistanciaTotal = t.CorredorAtual.DistanciaTotal,
- CorredorAtualIdx = t.CorredorAtual.Idx,
+
+ CorredorAtualDentro =
+ corredorAtual?.Dentro ?? false,
+ CorredorAtualDistanciaPercorrida =
+ corredorAtual?.DistanciaPercorridaCorredor ?? 0,
+ CorredorAtualDistanciaTotal =
+ corredorAtual?.DistanciaTotal ?? 0,
+ CorredorAtualIdx =
+ corredorAtual?.Idx ?? 0,
+
DistanciaDireita = t.DistanciaDireita,
DistanciaEsquerda = t.DistanciaEsquerda,
DistanciaPercorrida = t.DistanciaPercorrida,
@@ -637,260 +729,874 @@ namespace AgroBase.Models.Operacoes
NaMargemDoCorredor = t.NaMargemDoCorredor,
PercentualOperacao = t.PercentualOperacao,
PercentualRuaAtual = t.PercentualRuaAtual,
- PontoAtualDistanciaAtual = t.PontoAtual.DistanciaAtual,
- PontoAtualIdx = t.PontoAtual.idxPonto,
- PontoAtualIdxCorredor = t.PontoAtual.idxCorredor,
- ProximoPontoAproximando = t.ProximoPonto.Aproximando,
- ProximoPontoDistanciaAtual = t.ProximoPonto.DistanciaAtual,
- ProximoPontoIdx = t.ProximoPonto.idxPonto,
+
+ PontoAtualDistanciaAtual =
+ pontoAtual?.DistanciaAtual ?? 0,
+ PontoAtualIdx =
+ pontoAtual?.idxPonto ?? 0,
+ PontoAtualIdxCorredor =
+ pontoAtual?.idxCorredor ?? 0,
+
+ ProximoPontoAproximando =
+ proximoPonto?.Aproximando ?? false,
+ ProximoPontoDistanciaAtual =
+ proximoPonto?.DistanciaAtual ?? 0,
+ ProximoPontoIdx =
+ proximoPonto?.idxPonto ?? 0,
+
QtdPontos = t.QtdPontos,
StatusCarro = t.StatusCarro,
TempoEstimadoOperacao = t.TempoEstimadoOperacao
- } : new OperacaoParametrosDadosTrajetoriaModel();
- var c = op.Controle?.Clone();
- Controle = c != null ? new OperacaoParametrosDadosControleModel()
- {
- Angulo = c.Angulo,
- AlturaBarra = c.AlturaBarra,
- PercentualVelocidadeSPKmh = c.PercentualVelocidadeSPKmh,
- TipoMovimento = c.TipoMovimento,
- SimulacaoMPC = c.SimulacaoMPC,
- Motivos = c.Motivos
- } : new OperacaoParametrosDadosControleModel();
- ModulosSaude = new List(sen?.ModulosSaude ?? new List());
- DispositivosMapeados = new List(sen?.DispositivosMapeados ?? new List());
- Imu = new OperacaoSensoriamentoLogImuModel()
+ };
+ }
+
+ private void AtualizarControle(dynamic op)
+ {
+ var c = op?.Controle?.Clone();
+
+ Controle = c != null
+ ? new OperacaoParametrosDadosControleModel
+ {
+ Angulo = c.Angulo,
+ AlturaBarra = c.AlturaBarra,
+ PercentualVelocidadeSPKmh =
+ c.PercentualVelocidadeSPKmh,
+ TipoMovimento = c.TipoMovimento,
+ SimulacaoMPC = CloneList(c.SimulacaoMPC),
+ Motivos = CloneListString(c.Motivos)
+ }
+ : new OperacaoParametrosDadosControleModel();
+ }
+
+ private void AtualizarSaude(dynamic sen)
+ {
+ ModulosSaude = CloneList(
+ sen?.ModulosSaude
+ );
+
+ DispositivosMapeados = CloneList(
+ sen?.DispositivosMapeados
+ );
+ }
+
+ private void AtualizarImu(dynamic sen)
+ {
+ Imu = new OperacaoSensoriamentoLogImuModel
{
Iniciado = sen?.IMU?.Iniciado ?? false,
InclinacaoFrontal = sen?.IMU?.PitchSeguro ?? 0,
InclinacaoLateral = sen?.IMU?.RollSeguro ?? 0,
- Rotacao = sen?.IMU?.YawSeguro ?? 0,
+ Rotacao = sen?.IMU?.YawSeguro ?? 0
};
- Atuador = new OperacaoParametrosDadosAtuadorModel()
+ }
+
+ private void AtualizarAtuador(dynamic sen, dynamic dAtu)
+ {
+ var atu = sen?.Atuador;
+
+ var bicosPulverizadores = ToDynamicList(
+ dAtu == null ? null : dAtu.BicosPulverizadores
+ );
+
+ var bombasPressurizadoras = ToDynamicList(
+ dAtu == null ? null : dAtu.BombasPressurizadoras
+ );
+
+ var sensoresAtuador = ToDynamicList(
+ dAtu == null ? null : dAtu.Sensores
+ );
+
+ Atuador = new OperacaoParametrosDadosAtuadorModel
{
Iniciado = dAtu?.Conectado ?? false,
Latencia = dAtu?.DadosLeitura?.LatenciaLoop ?? 0,
-
- AnguloBarraDireita = sen?.Atuador?.AnguloBarraDireita ?? 0,
- AnguloBarraEsquerda = sen?.Atuador?.AnguloBarraEsquerda ?? 0,
- CapacidadeReservatorio = sen?.Atuador?.CapacidadeReservatorio ?? 0,
- ErvasNoRadar = sen?.Atuador?.ErvasNoRadar ?? false,
- DistanciaEstimadaRestanteMetros = sen?.Atuador?.DistanciaEstimadaRestanteMetros ?? 0,
- HerbicidaConsumidoMl = sen?.Atuador?.HerbicidaConsumido ?? 0,
- HerbicidaPorAtuacaoMl = sen?.Atuador?.HerbicidaPorErva ?? 0,
- LPorMetro = sen?.Atuador?.LPorMetro ?? 0,
- LPorMinuto = sen?.Atuador?.LPorMinuto ?? 0,
- MassaReservatorioKg = sen?.Atuador?.MassaReservatorio ?? 0,
- VolumeReservatorioL = sen?.Atuador?.VolumeReservatorio ?? 0,
- VolumeVazaoMl = sen?.Atuador?.VolumeVazaoMl ?? 0,
- VazaoMediaMLs = sen?.Atuador?.VazaoMedia ?? 0,
- VazaoInstantaneaMLs = sen?.Atuador?.VazaoInstantanea ?? 0,
- TempoEstimadoRestanteMinutos = sen?.Atuador?.TempoEstimadoRestanteMinutos ?? 0,
- QtdCamerasSolo = sen?.Atuador?.QtdCamerasSolo ?? 0,
- PercentualErvasNoRadar = sen?.Atuador?.PercentualErvasNoRadar ?? 0,
- PercentualErvasTerreno = sen?.Atuador?.PercentualErvasTerreno ?? 0,
- PercentualReservatorio = sen?.Atuador?.PercentualReservatorio ?? 0,
- PressaoLinhaPsi = sen?.Atuador?.PressaoLinha ?? 0,
- Bicos = dAtu?.BicosPulverizadores?.Select(x => new OperacaoParametrosDadosBicoModel()
- {
- ID = x.ID,
- ID_Num = x.ID_Num,
- Inicializado = x.Inicializado,
- Posicao = x.Posicao,
- AnguloAbertura = x.AnguloAbertura,
- ComandoAngulo = (int)x.AnguloControle,
- LeituraAngulo = (int)x._AnguloBicoLeitura,
- ComandoEstado = x.ComandoAtuar,
- LeituraEstado = x._EstadoLeitura == Estado.Ligado,
- VazaoMediaMLs = x.VazaoMediaMLs,
- VazaoInstantaneaMLs = x.VazaoInstantaneaMLs,
- VolumeVazadoML = x.VolumeVazadoML,
- QtdAtuacoes = x.Atuacoes,
- TempoAtuado = x.TempoAtuado,
- TrechosPulverizando = new List>(x.TrechosAtuado ?? new List>()),
- UltimoComandoRespondido = x.UltimaLeitura
- }).ToList(),
- Bombas = dAtu?.BombasPressurizadoras?.Select(x => new OperacaoParametrosDadosBombaModel()
- {
- ID = x.ID,
- ID_Num = x.ID_Num,
- Sensor_ID_Num = x.Sensor_ID_Num,
- Inicializado = x.Inicializado,
- ComandoEstado = x.ComandoAtuar,
- LeituraEstado = x._EstadoLeitura == Estado.Ligado,
- ComandoPressao = x.PressaoSP,
- LeituraPressao = x._PressaoAtual,
- ComandoPotencia = x.PotenciaSP,
- LeituraPotencia = x._Potencia,
- TempoAtuado = dAtu?.TempoAtuado ?? 0,
- UltimoComandoRespondido = x.UltimaLeitura,
- }).ToList(),
- Sensores = dAtu?.Sensores?.Select(x => new OperacaoParametrosDadosSensorModel()
- {
- Componente = x.Componente,
- ID = x.ID,
- ID_Num = x.ID_Num,
- Inicializado = x.Inicializado,
- Valores = x.ValoresLeituras?.Where(y => y.funcao != FuncoesPinout.Iniciado)?.Select(y => y.atual?.valor).ToList(),
- UltimoComandoRespondido = x.UltimaLeitura,
- }).ToList(),
- UltimoComandoRespondido = dAtu.DadosLeitura.UltimoComandoRespondido,
+
+ AnguloBarraDireita = atu?.AnguloBarraDireita ?? 0,
+ AnguloBarraEsquerda = atu?.AnguloBarraEsquerda ?? 0,
+ CapacidadeReservatorio =
+ atu?.CapacidadeReservatorio ?? 0,
+ ErvasNoRadar = atu?.ErvasNoRadar ?? false,
+ DistanciaEstimadaRestanteMetros =
+ atu?.DistanciaEstimadaRestanteMetros ?? 0,
+ HerbicidaConsumidoMl =
+ atu?.HerbicidaConsumido ?? 0,
+ HerbicidaPorAtuacaoMl =
+ atu?.HerbicidaPorErva ?? 0,
+ LPorMetro = atu?.LPorMetro ?? 0,
+ LPorMinuto = atu?.LPorMinuto ?? 0,
+ MassaReservatorioKg =
+ atu?.MassaReservatorio ?? 0,
+ VolumeReservatorioL =
+ atu?.VolumeReservatorio ?? 0,
+ VolumeVazaoMl = atu?.VolumeVazaoMl ?? 0,
+ VazaoMediaMLs = atu?.VazaoMedia ?? 0,
+ VazaoInstantaneaMLs =
+ atu?.VazaoInstantanea ?? 0,
+ TempoEstimadoRestanteMinutos =
+ atu?.TempoEstimadoRestanteMinutos ?? 0,
+ QtdCamerasSolo = atu?.QtdCamerasSolo ?? 0,
+ PercentualErvasNoRadar =
+ atu?.PercentualErvasNoRadar ?? 0,
+ PercentualErvasTerreno =
+ atu?.PercentualErvasTerreno ?? 0,
+ PercentualReservatorio =
+ atu?.PercentualReservatorio ?? 0,
+ PressaoLinhaPsi = atu?.PressaoLinha ?? 0,
+
+ Bicos =
+ dAtu?.BicosPulverizadores != null
+ ? ((IEnumerable)dAtu.BicosPulverizadores)
+ .Select(x => new OperacaoParametrosDadosBicoModel
+ {
+ ID = x.ID,
+ ID_Num = x.ID_Num,
+ Inicializado = x.Inicializado,
+ Posicao = x.Posicao,
+ AnguloAbertura = x.AnguloAbertura,
+ ComandoAngulo = (int)x.AnguloControle,
+ LeituraAngulo = (int)x._AnguloBicoLeitura,
+
+ /*
+ * Mantém a decisão correta que já fizemos:
+ * comando é mais confiável que leitura CAN de 1 s
+ * para métrica instantânea de atuação.
+ */
+ ComandoEstado = x.ComandoAtuar,
+ LeituraEstado = x._EstadoLeitura == Estado.Ligado,
+
+ VazaoMediaMLs = x.VazaoMediaMLs,
+ VazaoInstantaneaMLs = x.VazaoInstantaneaMLs,
+ VolumeVazadoML = x.VolumeVazadoML,
+ QtdAtuacoes = x.Atuacoes,
+ TempoAtuado = x.TempoAtuado,
+ TrechosPulverizando =
+ CloneTrechos(x.TrechosAtuado),
+ UltimoComandoRespondido = x.UltimaLeitura
+ })
+ .ToList()
+ : new List(),
+
+ Bombas =
+ dAtu?.BombasPressurizadoras != null
+ ? ((IEnumerable)dAtu.BombasPressurizadoras)
+ .Select(x => new OperacaoParametrosDadosBombaModel
+ {
+ ID = x.ID,
+ ID_Num = x.ID_Num,
+ Sensor_ID_Num = x.Sensor_ID_Num,
+ Inicializado = x.Inicializado,
+ ComandoEstado = x.ComandoAtuar,
+ LeituraEstado = x._EstadoLeitura == Estado.Ligado,
+ ComandoPressao = x.PressaoSP,
+ LeituraPressao = x._PressaoAtual,
+ ComandoPotencia = x.PotenciaSP,
+ LeituraPotencia = x._Potencia,
+ TempoAtuado = dAtu?.TempoAtuado ?? 0,
+ UltimoComandoRespondido = x.UltimaLeitura
+ })
+ .ToList()
+ : new List(),
+
+ Sensores =
+ dAtu?.Sensores != null
+ ? ((IEnumerable)dAtu.Sensores)
+ .Select(x => new OperacaoParametrosDadosSensorModel
+ {
+ Componente = x.Componente,
+ ID = x.ID,
+ ID_Num = x.ID_Num,
+ Inicializado = x.Inicializado,
+ Valores = ExtrairValoresSensor(x.ValoresLeituras),
+ UltimoComandoRespondido = x.UltimaLeitura
+ })
+ .ToList()
+ : new List(),
+
+ UltimoComandoRespondido =
+ dAtu?.DadosLeitura?.UltimoComandoRespondido
+ ?? DateTime.MinValue
};
- Sensoriamento = new OperacaoParametrosDadosSensoriamentoModel()
+ }
+
+ private void AtualizarSensoriamento(dynamic dSen)
+ {
+ Sensoriamento = new OperacaoParametrosDadosSensoriamentoModel
{
Iniciado = dSen?.Conectado ?? false,
- Latencia = dSen?.DadosLeitura?.LatenciaLoop ?? 0,
+ Latencia = dSen?.DadosLeitura?.LatenciaLoop ?? 0,
- Sensores = dSen?.Sensores?.Select(x => new OperacaoParametrosDadosSensorModel()
- {
- Componente = x.Componente,
- ID = x.ID,
- ID_Num = x.ID_Num,
- Inicializado = x.Inicializado,
- Valores = x.ValoresLeituras?.Where(y => y.funcao != FuncoesPinout.Iniciado)?.Select(y => y.atual?.valor).ToList(),
- UltimoComandoRespondido = x.UltimaLeitura,
- }).ToList(),
- Servos = dSen?.Servos?.Select(x => new OperacaoParametrosDadosServoModel()
- {
- ID = x.ID,
- ID_Num = x.ID_Num,
- Inicializado = x.Inicializado,
- Incremental = x.Incremental,
- ComandoAngulo = x._AnguloControle,
- LeituraAngulo = x._AnguloLeiutra,
- UltimoComandoRespondido = x.UltimaLeitura,
- }).ToList(),
- Reles = dSen?.Reles?.Select(x => new OperacaoParametrosDadosReleModel()
- {
- ID = x.ID,
- ID_Num = x.ID_Num,
- Inicializado = x.Inicializado,
- ComandoEstado = x.StatusControle == Estado.Ligado,
- LeituraEstado = x._EstadoLeitura == Estado.Ligado,
- UltimoComandoRespondido = x.UltimaLeitura,
- }).ToList(),
- Sinaleiros = dSen?.Sinaleiros?.Select(x => new OperacaoParametrosDadosSinaleiroModel()
- {
- ID = x.ID,
- ID_Num = x.ID_Num,
- Inicializado = x.Inicializado,
- Comportamento = x.Comportamento,
- StatusLedId = x._StatusLedId,
- StatusLed = x._StatusLed,
- UltimoComandoRespondido = x.UltimaLeitura
- }).ToList(),
- UltimoComandoRespondido = dSen.DadosLeitura.UltimoComandoRespondido
+ Sensores =
+ dSen?.Sensores != null
+ ? ((IEnumerable)dSen.Sensores)
+ .Select(x => new OperacaoParametrosDadosSensorModel
+ {
+ Componente = x.Componente,
+ ID = x.ID,
+ ID_Num = x.ID_Num,
+ Inicializado = x.Inicializado,
+ Valores = ExtrairValoresSensor(x.ValoresLeituras),
+ UltimoComandoRespondido = x.UltimaLeitura
+ })
+ .ToList()
+ : new List(),
+
+ Servos =
+ dSen?.Servos != null
+ ? ((IEnumerable)dSen.Servos)
+ .Select(x => new OperacaoParametrosDadosServoModel
+ {
+ ID = x.ID,
+ ID_Num = x.ID_Num,
+ Inicializado = x.Inicializado,
+ Incremental = x.Incremental,
+ ComandoAngulo = x._AnguloControle,
+ LeituraAngulo = x._AnguloLeiutra,
+ UltimoComandoRespondido = x.UltimaLeitura
+ })
+ .ToList()
+ : new List(),
+
+ Reles =
+ dSen?.Reles != null
+ ? ((IEnumerable)dSen.Reles)
+ .Select(x => new OperacaoParametrosDadosReleModel
+ {
+ ID = x.ID,
+ ID_Num = x.ID_Num,
+ Inicializado = x.Inicializado,
+ ComandoEstado =
+ x.StatusControle == Estado.Ligado,
+ LeituraEstado =
+ x._EstadoLeitura == Estado.Ligado,
+ UltimoComandoRespondido = x.UltimaLeitura
+ })
+ .ToList()
+ : new List(),
+
+ Sinaleiros =
+ dSen?.Sinaleiros != null
+ ? ((IEnumerable)dSen.Sinaleiros)
+ .Select(x => new OperacaoParametrosDadosSinaleiroModel
+ {
+ ID = x.ID,
+ ID_Num = x.ID_Num,
+ Inicializado = x.Inicializado,
+ Comportamento = x.Comportamento,
+ StatusLedId = x._StatusLedId,
+ StatusLed = x._StatusLed,
+ UltimoComandoRespondido = x.UltimaLeitura
+ })
+ .ToList()
+ : new List(),
+
+ UltimoComandoRespondido =
+ dSen?.DadosLeitura?.UltimoComandoRespondido
+ ?? DateTime.MinValue
};
- Movimentacao = new OperacaoParametrosDadosMovimentacaoModel()
+ }
+
+ private void AtualizarMovimentacao(dynamic sen, dynamic dMvd)
+ {
+ var modules = ToDynamicList(
+ dMvd == null ? null : dMvd.Modulos
+ );
+
+ var validMov =
+ ((IEnumerable)modules)
+ .Where(x => x?.MovMotor != null)
+ .ToList();
+
+ Movimentacao = new OperacaoParametrosDadosMovimentacaoModel
{
- VelocidadeMediaMs = sen?.Movimentacao?.VelocidadeMedia ?? 0,
- RPMMedio = sen?.Movimentacao?.RPMMedio ?? 0,
- TemperaturaMedia = sen?.Movimentacao?.TemperaturaMedia ?? 0,
- TempoMovimentoSegs = sen?.Movimentacao?.TempoMovimentoSegs ?? 0,
- DistanciaPercorridaParcial = sen?.Movimentacao?.DistanciaPercorridaParcial ?? 0,
- DistanciaPercorridaTotal = sen?.Movimentacao?.DistanciaPercorridaTotal ?? 0,
- Modulos = dMvd?.Modulos?.Select(x => new OperacaoSensoriamentoLogMovDadosModel()
- {
- Mod_ID = x.MovMotor.Mod_ID,
- ID = x.MovMotor._EnderecoCAN_Rx,
- Iniciado = x.MovMotor.Inicializado,
- Controlar = x.MovMotor.Comandar,
- CicloTrabalho = x.MovMotor.CicloTrabalho,
- Corrente_Barramento = x.MovMotor.Corrente_Barramento,
- Corrente_Motor = x.MovMotor.Corrente_Motor,
- Potencia = x.MovMotor.Potencia,
- RPM_Motor = x.MovMotor.RPM_Motor,
- RPM_Roda = x.MovMotor.RPM_Roda,
- TemperaturaDriver = x.MovMotor.TemperaturaDriver,
- Tensao = x.MovMotor.Tensao,
- VelocidadeInstantaneaMs = x.MovMotor.VelocidadeInstantanea,
- UltimoComandoRespondido = x.MovMotor.Leitura?.UltimoComandoRecebido ?? DateTime.MinValue,
- }).ToList(),
- UltimoComandoRespondido = dMvd.Modulos.Max(x => x.MovMotor.Leitura?.UltimoComandoRecebido ?? DateTime.MinValue)
+ VelocidadeMediaMs =
+ sen?.Movimentacao?.VelocidadeMedia ?? 0,
+ RPMMedio =
+ sen?.Movimentacao?.RPMMedio ?? 0,
+ TemperaturaMedia =
+ sen?.Movimentacao?.TemperaturaMedia ?? 0,
+ TempoMovimentoSegs =
+ sen?.Movimentacao?.TempoMovimentoSegs ?? 0,
+ DistanciaPercorridaParcial =
+ sen?.Movimentacao?.DistanciaPercorridaParcial ?? 0,
+ DistanciaPercorridaTotal =
+ sen?.Movimentacao?.DistanciaPercorridaTotal ?? 0,
+
+ Modulos =
+ validMov
+ .Select(x => new OperacaoSensoriamentoLogMovDadosModel
+ {
+ Mod_ID = x.MovMotor.Mod_ID,
+ ID = x.MovMotor._EnderecoCAN_Rx,
+ Iniciado = x.MovMotor.Inicializado,
+ Controlar = x.MovMotor.Comandar,
+ CicloTrabalho = x.MovMotor.CicloTrabalho,
+ Corrente_Barramento =
+ x.MovMotor.Corrente_Barramento,
+ Corrente_Motor =
+ x.MovMotor.Corrente_Motor,
+ Potencia = x.MovMotor.Potencia,
+ RPM_Motor = x.MovMotor.RPM_Motor,
+ RPM_Roda = x.MovMotor.RPM_Roda,
+ TemperaturaDriver =
+ x.MovMotor.TemperaturaDriver,
+ Tensao = x.MovMotor.Tensao,
+ VelocidadeInstantaneaMs =
+ x.MovMotor.VelocidadeInstantanea,
+ UltimoComandoRespondido =
+ x.MovMotor.Leitura?.UltimoComandoRecebido
+ ?? DateTime.MinValue
+ })
+ .ToList(),
+
+ UltimoComandoRespondido =
+ validMov.Any()
+ ? validMov.Max(x =>
+ x.MovMotor.Leitura?.UltimoComandoRecebido
+ ?? DateTime.MinValue)
+ : DateTime.MinValue
};
- Direcional = new OperacaoParametrosDadosDirecionalModel()
+ }
+
+ private void AtualizarDirecional(dynamic sen, dynamic dMvd)
+ {
+ var modules = ToDynamicList(
+ dMvd == null ? null : dMvd.Modulos
+ );
+
+ var validDir =
+ ((IEnumerable)modules)
+ .Where(x => x?.DirMotor != null)
+ .ToList();
+
+ Direcional = new OperacaoParametrosDadosDirecionalModel
{
- AnguloMedio = sen?.Direcional?.AnguloMedio ?? 0,
- Modulos = dMvd.Modulos.Select(x => new DirSensoriamentoLogModel()
- {
- Mod_ID = x.DirMotor.Mod_ID,
- ID = x.DirMotor._EnderecoCAN_Rx,
- Iniciado = x.DirMotor.Inicializado,
- Controlar = x.DirMotor.Comandar,
- AnguloDriver = x.DirMotor.AnguloDriver,
- AnguloFolgaCompensar = x.DirMotor.AnguloFolgaCompensar,
- AnguloLeitura = x.DirMotor.AnguloLeitura,
- AnguloReal = x.DirMotor.AnguloReal,
- Angulo_SP = x.DirMotor.Angulo_SP,
- FolgaMecanica = x.DirMotor.FolgaMecanica,
- Sentido = x.DirMotor.Sentido,
- SentidoReal = x.DirMotor.SentidoReal,
- Sentido_SP = x.DirMotor.Sentido_SP,
- Velocidade = x.DirMotor.Velocidade,
- UltimoComandoRespondido = x.DirMotor.Leitura?.UltimoComandoRecebido ?? DateTime.MinValue,
- }).ToList(),
- UltimoComandoRespondido = dMvd.Modulos.Max(x => x.DirMotor.Leitura?.UltimoComandoRecebido ?? DateTime.MinValue)
+ AnguloMedio =
+ sen?.Direcional?.AnguloMedio ?? 0,
+
+ Modulos =
+ validDir
+ .Select(x => new DirSensoriamentoLogModel
+ {
+ Mod_ID = x.DirMotor.Mod_ID,
+ ID = x.DirMotor._EnderecoCAN_Rx,
+ Iniciado = x.DirMotor.Inicializado,
+ Controlar = x.DirMotor.Comandar,
+ AnguloDriver = x.DirMotor.AnguloDriver,
+ AnguloFolgaCompensar =
+ x.DirMotor.AnguloFolgaCompensar,
+ AnguloLeitura = x.DirMotor.AnguloLeitura,
+ AnguloReal = x.DirMotor.AnguloReal,
+ Angulo_SP = x.DirMotor.Angulo_SP,
+ FolgaMecanica = x.DirMotor.FolgaMecanica,
+ Sentido = x.DirMotor.Sentido,
+ SentidoReal = x.DirMotor.SentidoReal,
+ Sentido_SP = x.DirMotor.Sentido_SP,
+ Velocidade = x.DirMotor.Velocidade,
+ UltimoComandoRespondido =
+ x.DirMotor.Leitura?.UltimoComandoRecebido
+ ?? DateTime.MinValue
+ })
+ .ToList(),
+
+ UltimoComandoRespondido =
+ validDir.Any()
+ ? validDir.Max(x =>
+ x.DirMotor.Leitura?.UltimoComandoRecebido
+ ?? DateTime.MinValue)
+ : DateTime.MinValue
};
+ }
+
+ private void AtualizarGnss(dynamic sen)
+ {
+ var gps = sen?.Gps;
+
+ Gnss = new OperacaoParametrosDadosGNSSModel
+ {
+ Iniciado = gps?.Inicializado ?? false,
+ Altitude = gps?.Altitude ?? 0,
+ Distancia = gps?.Distancia ?? 0,
+ IdadeCorrecao = gps?.IdadeCorrecao ?? -1,
+ Latitude = gps?.Latitude ?? 0,
+ Longitude = gps?.Longitude ?? 0,
+ LeverArmFrontal = gps?.LeverArmFrontal ?? 0,
+ LeverArmLateral = gps?.LeverArmLateral ?? 0,
+ NtripAtivado = gps?.Ntrip_ativado ?? false,
+ NumeroSatelites = gps?.NumeroSatelites ?? 0,
+ OrientacaoMovimento = gps?.OrientacaoMovimento ?? 0,
+ OrientacaoReal = gps?.OrientacaoReal ?? 0,
+ PrecisaoCm = gps?.PrecisaoCm ?? 0,
+ PrecisaoHorizontal = gps?.PrecisaoHorizontal ?? 0,
+ QualidadeFix =
+ gps?.QualidadeFix ?? TiposCorrecaoGPS.SemCorrecao,
+ Velocidade = gps?.Velocidade ?? 0,
+ UltimoComandoRespondido =
+ gps?.UltimoComandoRespondido ?? DateTime.MinValue
+ };
+ }
+
+ private void AtualizarOutrosBlocos(dynamic sen)
+ {
Bateria = sen?.Bateria?.Clone();
Refrigeracao = sen?.CoolerControl?.Clone();
- Gnss = new OperacaoParametrosDadosGNSSModel()
- {
- Iniciado = sen?.Gps?.Inicializado ?? false,
- Altitude = sen?.Gps?.Altitude ?? 0,
- Distancia = sen?.Gps?.Distancia ?? 0,
- IdadeCorrecao = sen?.Gps?.IdadeCorrecao ?? 0,
- Latitude = sen?.Gps?.Latitude ?? 0,
- Longitude = sen?.Gps?.Longitude ?? 0,
- LeverArmFrontal = sen?.Gps?.LeverArmFrontal ?? 0,
- LeverArmLateral = sen?.Gps?.LeverArmLateral ?? 0,
- NtripAtivado = sen?.Gps?.Ntrip_ativado ?? false,
- NumeroSatelites = sen?.Gps?.NumeroSatelites ?? 0,
- OrientacaoMovimento = sen?.Gps?.OrientacaoMovimento ?? 0,
- OrientacaoReal = sen?.Gps?.OrientacaoReal ?? 0,
- PrecisaoCm = sen?.Gps?.PrecisaoCm ?? 0,
- PrecisaoHorizontal = sen?.Gps?.PrecisaoHorizontal ?? 0,
- QualidadeFix = sen?.Gps?.QualidadeFix ?? TiposCorrecaoGPS.SemCorrecao,
- Velocidade = sen?.Gps?.Velocidade ?? 0,
- UltimoComandoRespondido = sen?.Gps?.UltimoComandoRespondido ?? DateTime.MinValue,
- };
PerformanceNcp = sen?.DadosPerformance?.Clone();
LivoxLidar = sen?.LivoxLidar?.Clone();
- CameraWorkerService.GetListaCameras();
- Cameras = CameraWorkerService.ListaCameras;
- OperadorVisual = VisualWorkerService.DadosLeitura?.Resumo?.Clone();
- var l = sen?.Logs;
- Logs = l != null ? l.Where(x => x.Momento >= UltimoEnvioLog).ToList() : new List();
+ OperadorVisual = VisualWorkerService
+ .DadosLeitura?
+ .Resumo?
+ .Clone();
}
-
+
+ private void AtualizarCameras()
+ {
+ CameraWorkerService.GetListaCameras();
+
+ Cameras = CameraWorkerService.ListaCameras != null
+ ? new List(
+ CameraWorkerService.ListaCameras
+ )
+ : new List();
+ }
+
+ private void AtualizarLogs(dynamic sen)
+ {
+ DateTime corte = UltimoEnvioLog;
+
+ var logs = sen?.Logs;
+
+ Logs = logs != null
+ ? ((IEnumerable)logs)
+ .Where(x => x != null && x.Momento > corte)
+ .ToList()
+ : new List();
+ }
+
public OperacaoParametrosDadosModel Clone()
{
- return new OperacaoParametrosDadosModel()
+ return new OperacaoParametrosDadosModel
{
Momento = Momento,
StatusRover = StatusRover,
TipoCanAtivo = TipoCanAtivo,
- Operacao = Operacao,
+
+ Operacao = CloneOperacao(Operacao),
Trajetoria = Trajetoria?.Clone(),
Controle = Controle?.Clone(),
- ModulosSaude = new List(ModulosSaude ?? new List()),
- DispositivosMapeados = new List(DispositivosMapeados ?? new List()),
+
+ ModulosSaude = CloneList(ModulosSaude),
+ DispositivosMapeados = CloneList(DispositivosMapeados),
+
Imu = Imu?.Clone(),
- Atuador = Atuador,
- Sensoriamento = Sensoriamento,
- Movimentacao = Movimentacao,
- Direcional = Direcional,
+ Atuador = CloneAtuador(Atuador),
+ Sensoriamento = CloneSensoriamento(Sensoriamento),
+ Movimentacao = CloneMovimentacao(Movimentacao),
+ Direcional = CloneDirecional(Direcional),
+
Bateria = Bateria?.Clone(),
Refrigeracao = Refrigeracao?.Clone(),
- Gnss = Gnss,
+ Gnss = CloneGnss(Gnss),
PerformanceNcp = PerformanceNcp?.Clone(),
LivoxLidar = LivoxLidar?.Clone(),
- Logs = new List(Logs ?? new List()),
- Cameras = new List(Cameras ?? new List()),
+
+ Logs = CloneList(Logs),
+ Cameras = Cameras != null
+ ? new List(Cameras)
+ : new List(),
+
OperadorVisual = OperadorVisual?.Clone(),
UltimoEnvioLog = UltimoEnvioLog
};
}
+ private static OperacaoParametrosDadosOperacaoModel CloneOperacao(
+ OperacaoParametrosDadosOperacaoModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosOperacaoModel
+ {
+ Modo = src.Modo,
+ Status = src.Status,
+ ImpedimentoErro = src.ImpedimentoErro,
+ Liberada = src.Liberada,
+ Iniciada = src.Iniciada,
+ Emergencia = src.Emergencia,
+ Pausa = src.Pausa,
+ IniciadoEm = src.IniciadoEm,
+ FinalizadoEm = src.FinalizadoEm,
+ TempoAguardandoSegs = src.TempoAguardandoSegs,
+ TempoDecorridoSegs = src.TempoDecorridoSegs,
+ ModulosMandatorios = CloneList(src.ModulosMandatorios),
+ ParametrosMandatorios = CloneList(src.ParametrosMandatorios)
+ };
+ }
+
+ private static OperacaoParametrosDadosAtuadorModel CloneAtuador(
+ OperacaoParametrosDadosAtuadorModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosAtuadorModel
+ {
+ Iniciado = src.Iniciado,
+ Latencia = src.Latencia,
+ CapacidadeReservatorio = src.CapacidadeReservatorio,
+ QtdCamerasSolo = src.QtdCamerasSolo,
+ ErvasNoRadar = src.ErvasNoRadar,
+ PercentualErvasNoRadar = src.PercentualErvasNoRadar,
+ PercentualErvasTerreno = src.PercentualErvasTerreno,
+ HerbicidaConsumidoMl = src.HerbicidaConsumidoMl,
+ HerbicidaPorAtuacaoMl = src.HerbicidaPorAtuacaoMl,
+ MassaReservatorioKg = src.MassaReservatorioKg,
+ VolumeReservatorioL = src.VolumeReservatorioL,
+ PercentualReservatorio = src.PercentualReservatorio,
+ PressaoLinhaPsi = src.PressaoLinhaPsi,
+ VazaoInstantaneaMLs = src.VazaoInstantaneaMLs,
+ VazaoMediaMLs = src.VazaoMediaMLs,
+ VolumeVazaoMl = src.VolumeVazaoMl,
+ LPorMetro = src.LPorMetro,
+ LPorMinuto = src.LPorMinuto,
+ TempoEstimadoRestanteMinutos =
+ src.TempoEstimadoRestanteMinutos,
+ DistanciaEstimadaRestanteMetros =
+ src.DistanciaEstimadaRestanteMetros,
+ AnguloBarraEsquerda = src.AnguloBarraEsquerda,
+ AnguloBarraDireita = src.AnguloBarraDireita,
+ Bicos = src.Bicos?.Select(CloneBico).ToList()
+ ?? new List(),
+ Bombas = src.Bombas?.Select(CloneBomba).ToList()
+ ?? new List(),
+ Sensores = src.Sensores?.Select(CloneSensor).ToList()
+ ?? new List(),
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosBicoModel CloneBico(
+ OperacaoParametrosDadosBicoModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosBicoModel
+ {
+ ID = src.ID,
+ ID_Num = src.ID_Num,
+ Inicializado = src.Inicializado,
+ Posicao = src.Posicao,
+ ComandoEstado = src.ComandoEstado,
+ LeituraEstado = src.LeituraEstado,
+ ComandoAngulo = src.ComandoAngulo,
+ LeituraAngulo = src.LeituraAngulo,
+ QtdAtuacoes = src.QtdAtuacoes,
+ TempoAtuado = src.TempoAtuado,
+ VazaoMediaMLs = src.VazaoMediaMLs,
+ VazaoInstantaneaMLs = src.VazaoInstantaneaMLs,
+ VolumeVazadoML = src.VolumeVazadoML,
+ AnguloAbertura = src.AnguloAbertura,
+ TrechosPulverizando = CloneTrechos(src.TrechosPulverizando),
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosBombaModel CloneBomba(
+ OperacaoParametrosDadosBombaModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosBombaModel
+ {
+ ID = src.ID,
+ ID_Num = src.ID_Num,
+ Sensor_ID_Num = src.Sensor_ID_Num,
+ Inicializado = src.Inicializado,
+ ComandoEstado = src.ComandoEstado,
+ LeituraEstado = src.LeituraEstado,
+ ComandoPressao = src.ComandoPressao,
+ LeituraPressao = src.LeituraPressao,
+ TempoAtuado = src.TempoAtuado,
+ ComandoPotencia = src.ComandoPotencia,
+ LeituraPotencia = src.LeituraPotencia,
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosSensoriamentoModel
+ CloneSensoriamento(OperacaoParametrosDadosSensoriamentoModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosSensoriamentoModel
+ {
+ Iniciado = src.Iniciado,
+ Latencia = src.Latencia,
+ Sinaleiros = src.Sinaleiros?.Select(CloneSinaleiro).ToList()
+ ?? new List(),
+ Sensores = src.Sensores?.Select(CloneSensor).ToList()
+ ?? new List(),
+ Servos = src.Servos?.Select(CloneServo).ToList()
+ ?? new List(),
+ Reles = src.Reles?.Select(CloneRele).ToList()
+ ?? new List(),
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosSensorModel CloneSensor(
+ OperacaoParametrosDadosSensorModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosSensorModel
+ {
+ Componente = src.Componente,
+ ID = src.ID,
+ ID_Num = src.ID_Num,
+ Inicializado = src.Inicializado,
+ Valores = src.Valores != null
+ ? new List(src.Valores)
+ : new List(),
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosServoModel CloneServo(
+ OperacaoParametrosDadosServoModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosServoModel
+ {
+ ID = src.ID,
+ ID_Num = src.ID_Num,
+ Inicializado = src.Inicializado,
+ Incremental = src.Incremental,
+ ComandoAngulo = src.ComandoAngulo,
+ LeituraAngulo = src.LeituraAngulo,
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosReleModel CloneRele(
+ OperacaoParametrosDadosReleModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosReleModel
+ {
+ ID = src.ID,
+ ID_Num = src.ID_Num,
+ Inicializado = src.Inicializado,
+ ComandoEstado = src.ComandoEstado,
+ LeituraEstado = src.LeituraEstado,
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosSinaleiroModel CloneSinaleiro(
+ OperacaoParametrosDadosSinaleiroModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosSinaleiroModel
+ {
+ ID = src.ID,
+ ID_Num = src.ID_Num,
+ Inicializado = src.Inicializado,
+ Comportamento = src.Comportamento,
+ StatusLedId = src.StatusLedId,
+ StatusLed = src.StatusLed,
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosMovimentacaoModel
+ CloneMovimentacao(OperacaoParametrosDadosMovimentacaoModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosMovimentacaoModel
+ {
+ VelocidadeMediaMs = src.VelocidadeMediaMs,
+ RPMMedio = src.RPMMedio,
+ TempoMovimentoSegs = src.TempoMovimentoSegs,
+ DistanciaPercorridaTotal = src.DistanciaPercorridaTotal,
+ DistanciaPercorridaParcial = src.DistanciaPercorridaParcial,
+ TemperaturaMedia = src.TemperaturaMedia,
+ Modulos = CloneList(src.Modulos),
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosDirecionalModel CloneDirecional(
+ OperacaoParametrosDadosDirecionalModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosDirecionalModel
+ {
+ AnguloMedio = src.AnguloMedio,
+ Modulos = CloneList(src.Modulos),
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static OperacaoParametrosDadosGNSSModel CloneGnss(
+ OperacaoParametrosDadosGNSSModel src)
+ {
+ if (src == null)
+ return null;
+
+ return new OperacaoParametrosDadosGNSSModel
+ {
+ Iniciado = src.Iniciado,
+ Latitude = src.Latitude,
+ Longitude = src.Longitude,
+ Altitude = src.Altitude,
+ Velocidade = src.Velocidade,
+ Distancia = src.Distancia,
+ OrientacaoMovimento = src.OrientacaoMovimento,
+ OrientacaoReal = src.OrientacaoReal,
+ NumeroSatelites = src.NumeroSatelites,
+ PrecisaoHorizontal = src.PrecisaoHorizontal,
+ PrecisaoCm = src.PrecisaoCm,
+ QualidadeFix = src.QualidadeFix,
+ NtripAtivado = src.NtripAtivado,
+ IdadeCorrecao = src.IdadeCorrecao,
+ LeverArmLateral = src.LeverArmLateral,
+ LeverArmFrontal = src.LeverArmFrontal,
+ UltimoComandoRespondido = src.UltimoComandoRespondido
+ };
+ }
+
+ private static List CloneList(IEnumerable src)
+ {
+ return src != null
+ ? new List(src)
+ : new List();
+ }
+
+ private static List CloneListString(IEnumerable src)
+ {
+ return src != null
+ ? src.Where(x => x != null).ToList()
+ : new List();
+ }
+
+ private static List ToDynamicList(object src)
+ {
+ var result = new List();
+
+ if (src == null)
+ return result;
+
+ var enumerable = src as System.Collections.IEnumerable;
+
+ if (enumerable == null)
+ return result;
+
+ foreach (var item in enumerable)
+ result.Add(item);
+
+ return result;
+ }
+
+ private static List ExtrairValoresSensor(dynamic valoresLeituras)
+ {
+ var result = new List();
+
+ if (valoresLeituras == null)
+ return result;
+
+ foreach (var item in valoresLeituras)
+ {
+ try
+ {
+ if (item.funcao == FuncoesPinout.Iniciado)
+ continue;
+
+ result.Add(item.atual?.valor);
+ }
+ catch { }
+ }
+
+ return result;
+ }
+
+ private static List> CloneTrechos(dynamic trechos)
+ {
+ var result = new List>();
+
+ if (trechos == null)
+ return result;
+
+ foreach (var trecho in trechos)
+ {
+ if (trecho == null)
+ {
+ result.Add(new List());
+ continue;
+ }
+
+ var novoTrecho = new List();
+
+ foreach (var ponto in trecho)
+ {
+ /*
+ * Sem assumir que PontoGeo tem Clone().
+ * Copia a lista interna para evitar mutação estrutural.
+ * Se PontoGeo for mutável, o próximo passo é adicionar
+ * Clone() nele também.
+ */
+ novoTrecho.Add(ponto);
+ }
+
+ result.Add(novoTrecho);
+ }
+
+ return result;
+ }
+
+ private static double TryGetDouble(
+ object obj,
+ string property,
+ double fallback)
+ {
+ if (obj == null ||
+ string.IsNullOrWhiteSpace(property))
+ {
+ return fallback;
+ }
+
+ try
+ {
+ var prop = obj.GetType().GetProperty(property);
+
+ if (prop == null)
+ return fallback;
+
+ object value = prop.GetValue(obj, null);
+
+ if (value == null)
+ return fallback;
+
+ return Convert.ToDouble(value);
+ }
+ catch
+ {
+ return fallback;
+ }
+ }
}
public class OperacaoParametrosDadosOperacaoModel
@@ -937,7 +1643,7 @@ namespace AgroBase.Models.Operacoes
public OperacaoParametrosDadosTrajetoriaModel Clone()
{
- return new OperacaoParametrosDadosTrajetoriaModel()
+ return new OperacaoParametrosDadosTrajetoriaModel
{
TempoEstimadoOperacao = TempoEstimadoOperacao,
StatusCarro = StatusCarro,
@@ -945,10 +1651,33 @@ namespace AgroBase.Models.Operacoes
ProximoPontoIdx = ProximoPontoIdx,
ProximoPontoDistanciaAtual = ProximoPontoDistanciaAtual,
AnguloCaminho = AnguloCaminho,
- AutonomiaCorredor = AutonomiaCorredor,
+ AutonomiaCorredor =
+ AutonomiaCorredor == null
+ ? null
+ : new OperacaoParametrosDadosTrajetoriaAutonomiaCorredorModel
+ {
+ Status = AutonomiaCorredor.Status,
+ DistanciaCorredor_m =
+ AutonomiaCorredor.DistanciaCorredor_m,
+ DistanciaSeguraBateria_m =
+ AutonomiaCorredor.DistanciaSeguraBateria_m,
+ DistanciaSeguraHerbicida_m =
+ AutonomiaCorredor.DistanciaSeguraHerbicida_m,
+ BateriaSuficiente =
+ AutonomiaCorredor.BateriaSuficiente,
+ HerbicidaSuficiente =
+ AutonomiaCorredor.HerbicidaSuficiente,
+ Liberado = AutonomiaCorredor.Liberado,
+ Motivo = AutonomiaCorredor.Motivo,
+ Motivos = AutonomiaCorredor.Motivos != null
+ ? new List(AutonomiaCorredor.Motivos)
+ : new List()
+ },
CorredorAtualDentro = CorredorAtualDentro,
- CorredorAtualDistanciaPercorrida = CorredorAtualDistanciaPercorrida,
- CorredorAtualDistanciaTotal = CorredorAtualDistanciaTotal,
+ CorredorAtualDistanciaPercorrida =
+ CorredorAtualDistanciaPercorrida,
+ CorredorAtualDistanciaTotal =
+ CorredorAtualDistanciaTotal,
CorredorAtualIdx = CorredorAtualIdx,
DistanciaDireita = DistanciaDireita,
DistanciaEsquerda = DistanciaEsquerda,
@@ -960,7 +1689,7 @@ namespace AgroBase.Models.Operacoes
PontoAtualDistanciaAtual = PontoAtualDistanciaAtual,
PontoAtualIdx = PontoAtualIdx,
PontoAtualIdxCorredor = PontoAtualIdxCorredor,
- ProximoPontoAproximando = ProximoPontoAproximando,
+ ProximoPontoAproximando = ProximoPontoAproximando
};
}
}
@@ -990,14 +1719,18 @@ namespace AgroBase.Models.Operacoes
public OperacaoParametrosDadosControleModel Clone()
{
- return new OperacaoParametrosDadosControleModel()
+ return new OperacaoParametrosDadosControleModel
{
AlturaBarra = AlturaBarra,
Angulo = Angulo,
PercentualVelocidadeSPKmh = PercentualVelocidadeSPKmh,
TipoMovimento = TipoMovimento,
- SimulacaoMPC = SimulacaoMPC,
- Motivos = new List(Motivos ?? new List())
+ SimulacaoMPC = SimulacaoMPC != null
+ ? new List(SimulacaoMPC)
+ : new List(),
+ Motivos = Motivos != null
+ ? new List(Motivos)
+ : new List()
};
}
}
diff --git a/AgroBase/AgroBase/Models/Variaveis.cs b/AgroBase/AgroBase/Models/Variaveis.cs
index 5275a6d7f..0b46294bf 100644
--- a/AgroBase/AgroBase/Models/Variaveis.cs
+++ b/AgroBase/AgroBase/Models/Variaveis.cs
@@ -1,4 +1,4 @@
-using AgroBase.Models.Modules;
+using AgroBase.Models.Modules;
using AgroBase.Services;
using System;
using System.Collections.Generic;
@@ -58,8 +58,39 @@ namespace AgroBase.Models
public static string CaminhoModelos { get; } = "C:\\AgroBaseModels\\";
public static List DispositivosConectados { get; set; } = new List();
public static OperacaoModel OperacaoEmAndamento { get; set; } = new OperacaoModel();
- public static MqttService MqttServiceLocal { get; set; }
- public static MqttService MqttServiceBase { get; set; }
+ public static MqttService MqttServiceLocal { get; private set; }
+ public static MqttService MqttServiceBaseCritical { get; private set; }
+ public static MqttService MqttServiceBaseTelemetry { get; private set; }
+
+ ///
+ /// Compatibilidade temporária com o restante do projeto.
+ /// Publicações antigas feitas por MqttServiceBase seguem pelo canal
+ /// de telemetria, nunca pelo canal crítico de RTCM/comandos.
+ ///
+ public static MqttService MqttServiceBase
+ {
+ get { return MqttServiceBaseTelemetry; }
+ set { MqttServiceBaseTelemetry = value; }
+ }
+
+ public static BaseLinkState BaseLink { get; } = new BaseLinkState();
+
+ public static MqttService.MqttTopicosModel TopicoLocalCoordenadasGps { get; private set; }
+ public static MqttService.MqttTopicosModel TopicoLocalTrajetoriaDinamica { get; private set; }
+ public static MqttService.MqttTopicosModel TopicoLocalSelecaoRuas { get; private set; }
+
+ public static MqttService.MqttTopicosModel TopicoBaseDiscovery { get; private set; }
+ public static MqttService.MqttTopicosModel TopicoBaseTelemetria { get; private set; }
+ public static MqttService.MqttTopicosModel TopicoBaseParametros { get; private set; }
+
+ public static MqttService.MqttTopicosModel TopicoBaseHeartbeat { get; private set; }
+ public static MqttService.MqttTopicosModel TopicoBaseComandos { get; private set; }
+ public static MqttService.MqttTopicosModel TopicoBaseRtcm { get; private set; }
+ public static MqttService.MqttTopicosModel TopicoBasePosicao { get; private set; }
+
+ private static readonly SemaphoreSlim _mqttLifecycleLock = new SemaphoreSlim(1, 1);
+ private static AsyncTaskTimerModel _tmrMqttLinkMonitor;
+ private static int _mqttCriticalConnectedState = -1;
public static LoRaEspService LoraService
{
get
@@ -91,90 +122,450 @@ namespace AgroBase.Models
public static byte ID_Num_sTOD { get; } = 250;
public static byte ID_Num_sLRA { get; } = 251;
- public static async void IniciarMQTT()
+ ///
+ /// Nome antigo preservado para compatibilidade. Novos pontos de startup
+ /// devem aguardar IniciarMqttAsync diretamente.
+ ///
+ public static Task IniciarMQTT()
{
- if (MqttServiceLocal != null)
- {
- foreach (var topico in MqttServiceLocal.Topicos.Where(x => x.Inscrever))
- {
- await MqttServiceLocal.UnsubscribeAsync(topico);
- }
- MqttServiceLocal.Topicos.Clear();
- }
+ return IniciarMqttAsync();
+ }
- if (MqttServiceBase != null)
- {
- foreach (var topico in MqttServiceBase.Topicos.Where(x => x.Inscrever))
- {
- await MqttServiceBase.UnsubscribeAsync(topico);
- }
- MqttServiceBase.Topicos.Clear();
- }
+ ///
+ /// Inicializa deterministicamente os clientes MQTT e seus tópicos.
+ /// Uma segunda chamada encerra por completo a geração anterior antes
+ /// de criar novos clientes.
+ ///
+ public static async Task IniciarMqttAsync(
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ await _mqttLifecycleLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- MqttServiceLocal = new MqttService("localhost", 1883, VariaveisEquipamento.Parametros.serial_number, true, msg => Console.WriteLine($"[MQTT localhost:{1883}] - {msg}"));
- await MqttServiceLocal.AdicionarNovoTopico(MapasVariaveisModel.TopicoCoordenadasGPS);
- await MqttServiceLocal.AdicionarNovoTopico(MapasVariaveisModel.TopicoTrajetoriaDinamica);
- await MqttServiceLocal.AdicionarNovoTopico(MapasVariaveisModel.TopicoSelecaoRuasMapa, true, 1, async (message) =>
+ try
{
- OperacaoEmAndamento.Mapa.AtualizarRuasSelecionadas();
- });
-
- MqttServiceBase = new MqttService(VariaveisEquipamento.Parametros.base_ip, 1883, VariaveisEquipamento.Parametros.serial_number, false, msg => Console.WriteLine($"[MQTT {VariaveisEquipamento.Parametros.base_ip}:{1883}] - {msg}"));
- await MqttServiceBase.AdicionarNovoTopico(VariaveisMonitoramento.TopicoMqttDispositivos);
- await MqttServiceBase.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttTelemetria.Replace("", VariaveisEquipamento.Parametros.serial_number));
- await MqttServiceBase.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttParametros.Replace("", VariaveisEquipamento.Parametros.serial_number));
- await MqttServiceBase.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttHeartbeat.Replace("", VariaveisEquipamento.Parametros.serial_number), true, 1, async (message) =>
- {
- if (string.IsNullOrEmpty(message.Mensagem)) return;
- VariaveisOperacao.PosicaoBase.UltimoComandoRespondido = DateTime.Now;
- });
- await MqttServiceBase.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttComandos.Replace("", VariaveisEquipamento.Parametros.serial_number), true, 1, async (message) =>
- {
- if (string.IsNullOrEmpty(message.Mensagem)) return;
- try
- {
- string json = message.Mensagem;
- var cmd = JsonConvert.DeserializeObject(json);
- OperacaoEmAndamento.ExecutaComandoDaBase(cmd);
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Erro ao deserializar comando da base: {ex.Message}");
- }
- });
- await MqttServiceBase.AdicionarNovoTopico(VariaveisMonitoramento.TopicoMqttRTCM, true, 1, async (message) =>
- {
- if (message.Bytes == null || message.Bytes.Length == 0)
+ if (Fechando)
return;
- try
+
+ string roverId = VariaveisEquipamento.Parametros.serial_number;
+ string baseIp = VariaveisEquipamento.Parametros.base_ip;
+
+ if (string.IsNullOrWhiteSpace(roverId))
+ throw new InvalidOperationException("O serial_number do rover não foi configurado.");
+
+ if (string.IsNullOrWhiteSpace(baseIp))
+ throw new InvalidOperationException("O base_ip do rover não foi configurado.");
+
+ await EncerrarMqttInternoAsync("Reinicialização MQTT").ConfigureAwait(false);
+
+ BaseLink.BeginNewSession(roverId);
+ Interlocked.Exchange(ref _mqttCriticalConnectedState, -1);
+
+ MqttServiceLocal = new MqttService(
+ "localhost",
+ 1883,
+ roverId,
+ true,
+ msg => Console.WriteLine($"[MQTT LOCAL localhost:1883] - {msg}"));
+
+ MqttServiceBaseCritical = new MqttService(
+ baseIp,
+ 1883,
+ roverId + "-critical",
+ false,
+ msg => Console.WriteLine($"[MQTT CRITICAL {baseIp}:1883] - {msg}"));
+
+ MqttServiceBaseTelemetry = new MqttService(
+ baseIp,
+ 1883,
+ roverId + "-telemetry",
+ false,
+ msg => Console.WriteLine($"[MQTT TELEMETRY {baseIp}:1883] - {msg}"));
+
+ await ConfigurarTopicosMqttLocalAsync().ConfigureAwait(false);
+ await ConfigurarTopicosMqttCriticosAsync(roverId).ConfigureAwait(false);
+ await ConfigurarTopicosMqttTelemetriaAsync(roverId).ConfigureAwait(false);
+
+ _tmrMqttLinkMonitor = new AsyncTaskTimerModel(
+ "tmrMqttLinkMonitor",
+ MonitorarLinkMqttAsync,
+ interval: 500,
+ timeout: 2000,
+ scheduleMode: AsyncTaskTimerScheduleMode.FixedRateSkipMissed,
+ runImmediately: true);
+
+ _tmrMqttLinkMonitor.DebugMessages = false;
+ _tmrMqttLinkMonitor.Start();
+
+ await Task.WhenAll(
+ MqttServiceLocal.StartAsync(),
+ MqttServiceBaseCritical.StartAsync(),
+ MqttServiceBaseTelemetry.StartAsync()
+ ).ConfigureAwait(false);
+
+ AtualizarEstadoBrokerCritico();
+ }
+ catch (Exception ex)
+ {
+ BaseLink.RecordError("Falha ao inicializar MQTT: " + ex.Message);
+ Console.WriteLine("[MQTT] Falha durante inicialização: " + ex);
+
+ await EncerrarMqttInternoAsync(
+ "Falha durante inicialização MQTT"
+ ).ConfigureAwait(false);
+
+ throw;
+ }
+ finally
+ {
+ _mqttLifecycleLock.Release();
+ }
+ }
+
+ public static async Task EncerrarMqttAsync()
+ {
+ await _mqttLifecycleLock.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ await EncerrarMqttInternoAsync("Encerramento solicitado").ConfigureAwait(false);
+ }
+ finally
+ {
+ _mqttLifecycleLock.Release();
+ }
+ }
+
+ private static async Task ConfigurarTopicosMqttLocalAsync()
+ {
+ TopicoLocalCoordenadasGps = await MqttServiceLocal.AdicionarNovoTopico(
+ MapasVariaveisModel.TopicoCoordenadasGPS,
+ inscrever: false,
+ mensagensManter: 0,
+ callback: null,
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.LatestOnly,
+ queueCapacity: 1,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtMostOnce,
+ retain: false
+ ).ConfigureAwait(false);
+
+ TopicoLocalTrajetoriaDinamica = await MqttServiceLocal.AdicionarNovoTopico(
+ MapasVariaveisModel.TopicoTrajetoriaDinamica,
+ inscrever: false,
+ mensagensManter: 0,
+ callback: null,
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.LatestOnly,
+ queueCapacity: 1,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtMostOnce,
+ retain: false
+ ).ConfigureAwait(false);
+
+ TopicoLocalSelecaoRuas = await MqttServiceLocal.AdicionarNovoTopico(
+ MapasVariaveisModel.TopicoSelecaoRuasMapa,
+ inscrever: true,
+ mensagensManter: 1,
+ callback: message =>
{
- var bytes = message.Bytes;
- if (bytes != null && bytes.Length > 0)
+ OperacaoEmAndamento?.Mapa?.AtualizarRuasSelecionadas();
+ return Task.CompletedTask;
+ },
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.LatestOnly,
+ queueCapacity: 1,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtLeastOnce,
+ retain: false
+ ).ConfigureAwait(false);
+ }
+
+ private static async Task ConfigurarTopicosMqttCriticosAsync(string roverId)
+ {
+ TopicoBaseHeartbeat = await MqttServiceBaseCritical.AdicionarNovoTopico(
+ VariaveisEquipamento.TopicoMqttHeartbeat.Replace("", roverId),
+ inscrever: true,
+ mensagensManter: 1,
+ callback: message =>
+ {
+ MarcarBrokerCriticoConectado();
+ BaseLink.MarkHeartbeat();
+
+ // Compatibilidade temporária com tmrComunicacao_Tick.
+ // Será removido quando discovery/telemetria migrarem para
+ // serviços próprios baseados em BaseLinkState.
+ VariaveisOperacao.MarcarHeartbeatBaseLegado(DateTime.Now);
+ return Task.CompletedTask;
+ },
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.Inline,
+ queueCapacity: 1,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtMostOnce,
+ retain: false
+ ).ConfigureAwait(false);
+
+ TopicoBaseComandos = await MqttServiceBaseCritical.AdicionarNovoTopico(
+ VariaveisEquipamento.TopicoMqttComandos.Replace("", roverId),
+ inscrever: true,
+ mensagensManter: 2,
+ callback: message =>
+ {
+ if (string.IsNullOrWhiteSpace(message.Mensagem))
+ return Task.CompletedTask;
+
+ try
{
+ OperacaoComandoBaseModel cmd =
+ JsonConvert.DeserializeObject(message.Mensagem);
+
+ if (cmd == null)
+ return Task.CompletedTask;
+
+ MarcarBrokerCriticoConectado();
+ BaseLink.MarkCommand();
+ OperacaoEmAndamento?.ExecutaComandoDaBase(cmd);
+ }
+ catch (Exception ex)
+ {
+ BaseLink.RecordError("Erro no comando MQTT: " + ex.Message);
+ Console.WriteLine("Erro ao deserializar comando da base: " + ex.Message);
+ }
+
+ return Task.CompletedTask;
+ },
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.Sequential,
+ queueCapacity: 64,
+ overflowPolicy: MqttQueueOverflowPolicy.DropNewest,
+ qos: MqttQosLevel.AtLeastOnce,
+ retain: false
+ ).ConfigureAwait(false);
+
+ TopicoBaseRtcm = await MqttServiceBaseCritical.AdicionarNovoTopico(
+ VariaveisMonitoramento.TopicoMqttRTCM,
+ inscrever: true,
+ mensagensManter: 1,
+ callback: message =>
+ {
+ byte[] bytes = message.Bytes;
+
+ if (bytes == null || bytes.Length == 0)
+ return Task.CompletedTask;
+
+ try
+ {
+ MarcarBrokerCriticoConectado();
+ BaseLink.MarkRtcm();
GPSService.AplicarCorrecaoRTK_Mqtt(bytes, bytes.Length);
}
- }
- catch (Exception ex)
+ catch (Exception ex)
+ {
+ BaseLink.RecordError("Erro ao aplicar RTCM MQTT: " + ex.Message);
+ Console.WriteLine("Erro ao aplicar RTCM da base: " + ex.Message);
+ }
+
+ return Task.CompletedTask;
+ },
+ payloadType: MqttPayloadType.Binary,
+ dispatchMode: MqttDispatchMode.Sequential,
+ queueCapacity: 32,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtMostOnce,
+ retain: false
+ ).ConfigureAwait(false);
+
+ TopicoBasePosicao = await MqttServiceBaseCritical.AdicionarNovoTopico(
+ VariaveisMonitoramento.TopicoMqttPosicao,
+ inscrever: true,
+ mensagensManter: 1,
+ callback: message =>
{
- Console.WriteLine($"Erro ao deserializar RTCM da base: {ex.Message}");
- }
- });
- await MqttServiceBase.AdicionarNovoTopico(VariaveisMonitoramento.TopicoMqttPosicao, true, 1, async (message) =>
+ if (string.IsNullOrWhiteSpace(message.Mensagem))
+ return Task.CompletedTask;
+
+ try
+ {
+ GPSModel posicao = JsonConvert.DeserializeObject(message.Mensagem);
+
+ if (posicao == null)
+ return Task.CompletedTask;
+
+ posicao.Momento = DateTime.Now;
+ VariaveisOperacao.AtualizarPosicaoBase(posicao);
+
+ MarcarBrokerCriticoConectado();
+ BaseLink.MarkPosition();
+ }
+ catch (Exception ex)
+ {
+ BaseLink.RecordError("Erro na posição MQTT da base: " + ex.Message);
+ Console.WriteLine("Erro ao deserializar dados da base: " + ex.Message);
+ }
+
+ return Task.CompletedTask;
+ },
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.LatestOnly,
+ queueCapacity: 1,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtMostOnce,
+ retain: false
+ ).ConfigureAwait(false);
+ }
+
+ private static async Task ConfigurarTopicosMqttTelemetriaAsync(string roverId)
+ {
+ TopicoBaseDiscovery = await MqttServiceBaseTelemetry.AdicionarNovoTopico(
+ VariaveisMonitoramento.TopicoMqttDispositivos,
+ inscrever: false,
+ mensagensManter: 0,
+ callback: null,
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.LatestOnly,
+ queueCapacity: 1,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtMostOnce,
+ retain: false
+ ).ConfigureAwait(false);
+
+ TopicoBaseTelemetria = await MqttServiceBaseTelemetry.AdicionarNovoTopico(
+ VariaveisEquipamento.TopicoMqttTelemetria.Replace("", roverId),
+ inscrever: false,
+ mensagensManter: 0,
+ callback: null,
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.LatestOnly,
+ queueCapacity: 1,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtMostOnce,
+ retain: false
+ ).ConfigureAwait(false);
+
+ TopicoBaseParametros = await MqttServiceBaseTelemetry.AdicionarNovoTopico(
+ VariaveisEquipamento.TopicoMqttParametros.Replace("", roverId),
+ inscrever: false,
+ mensagensManter: 0,
+ callback: null,
+ payloadType: MqttPayloadType.Text,
+ dispatchMode: MqttDispatchMode.LatestOnly,
+ queueCapacity: 1,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtLeastOnce,
+ retain: false
+ ).ConfigureAwait(false);
+ }
+
+ private static Task MonitorarLinkMqttAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ AtualizarEstadoBrokerCritico();
+ return Task.CompletedTask;
+ }
+
+ private static void AtualizarEstadoBrokerCritico()
+ {
+ bool conectado = MqttServiceBaseCritical?.StatusConexao() == true;
+
+ if (conectado)
+ {
+ MarcarBrokerCriticoConectado();
+ return;
+ }
+
+ int anterior = Interlocked.Exchange(ref _mqttCriticalConnectedState, 0);
+
+ // O estado inicial desconectado não precisa ser contado como queda.
+ if (anterior == 1)
+ {
+ string motivo = MqttServiceBaseCritical?
+ .GetMetrics()?
+ .LastError;
+
+ BaseLink.MarkBrokerDisconnected(
+ string.IsNullOrWhiteSpace(motivo)
+ ? "Cliente MQTT crítico desconectado"
+ : motivo
+ );
+ }
+ }
+
+ private static void MarcarBrokerCriticoConectado()
+ {
+ int anterior = Interlocked.Exchange(ref _mqttCriticalConnectedState, 1);
+
+ if (anterior != 1)
+ BaseLink.MarkBrokerConnected();
+ }
+
+ private static async Task EncerrarMqttInternoAsync(string motivo)
+ {
+ AsyncTaskTimerModel monitor = _tmrMqttLinkMonitor;
+ _tmrMqttLinkMonitor = null;
+
+ if (monitor != null)
{
- if (string.IsNullOrEmpty(message.Mensagem))
- return;
try
{
- string json = message.Mensagem;
- var posicao = JsonConvert.DeserializeObject(json);
- posicao.Momento = DateTime.Now;
- VariaveisOperacao.PosicaoBase = posicao;
+ await monitor.DisposeAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
- Console.WriteLine($"Erro ao deserializar dados da base: {ex.Message}");
+ Console.WriteLine("[MQTT] Erro ao encerrar monitor do link: " + ex.Message);
}
- });
+ }
+
+ MqttService local = MqttServiceLocal;
+ MqttService critical = MqttServiceBaseCritical;
+ MqttService telemetry = MqttServiceBaseTelemetry;
+
+ MqttServiceLocal = null;
+ MqttServiceBaseCritical = null;
+ MqttServiceBaseTelemetry = null;
+
+ LimparReferenciasTopicosMqtt();
+
+ MqttService[] services = new[] { local, critical, telemetry }
+ .Where(x => x != null)
+ .Distinct()
+ .ToArray();
+
+ foreach (MqttService service in services)
+ {
+ try
+ {
+ await service.DisposeAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(
+ "[MQTT] Erro ao encerrar cliente '" +
+ service.ClientId + "': " + ex.Message
+ );
+ }
+ }
+
+ int anterior = Interlocked.Exchange(ref _mqttCriticalConnectedState, 0);
+
+ if (anterior == 1 || BaseLink.BrokerConnected)
+ BaseLink.MarkBrokerDisconnected(motivo);
+ }
+
+ private static void LimparReferenciasTopicosMqtt()
+ {
+ TopicoLocalCoordenadasGps = null;
+ TopicoLocalTrajetoriaDinamica = null;
+ TopicoLocalSelecaoRuas = null;
+
+ TopicoBaseDiscovery = null;
+ TopicoBaseTelemetria = null;
+ TopicoBaseParametros = null;
+
+ TopicoBaseHeartbeat = null;
+ TopicoBaseComandos = null;
+ TopicoBaseRtcm = null;
+ TopicoBasePosicao = null;
}
@@ -894,7 +1285,64 @@ namespace AgroBase.Models
return 0.7;
}
}
- public static GPSModel PosicaoBase { get; set; } = new GPSModel();
+ private static readonly object _posicaoBaseLock = new object();
+ private static GPSModel _posicaoBase = new GPSModel();
+
+ ///
+ /// Propriedade mantida por compatibilidade. Novos callbacks devem usar
+ /// AtualizarPosicaoBase e leituras concorrentes devem usar
+ /// ObterPosicaoBaseSnapshot.
+ ///
+ public static GPSModel PosicaoBase
+ {
+ get
+ {
+ lock (_posicaoBaseLock)
+ return _posicaoBase;
+ }
+ set
+ {
+ lock (_posicaoBaseLock)
+ _posicaoBase = value ?? new GPSModel();
+ }
+ }
+
+ public static void AtualizarPosicaoBase(GPSModel posicao)
+ {
+ if (posicao == null)
+ return;
+
+ lock (_posicaoBaseLock)
+ {
+ // Compatibilidade temporária: a lógica antiga de discovery
+ // ainda consulta este campo. Uma posição nova não deve apagar
+ // o heartbeat recebido anteriormente.
+ DateTime heartbeatAnterior =
+ _posicaoBase?.UltimoComandoRespondido ?? DateTime.MinValue;
+
+ if (posicao.UltimoComandoRespondido < heartbeatAnterior)
+ posicao.UltimoComandoRespondido = heartbeatAnterior;
+
+ _posicaoBase = posicao;
+ }
+ }
+
+ public static void MarcarHeartbeatBaseLegado(DateTime momento)
+ {
+ lock (_posicaoBaseLock)
+ {
+ if (_posicaoBase == null)
+ _posicaoBase = new GPSModel();
+
+ _posicaoBase.UltimoComandoRespondido = momento;
+ }
+ }
+
+ public static GPSModel ObterPosicaoBaseSnapshot()
+ {
+ lock (_posicaoBaseLock)
+ return _posicaoBase?.Clone() ?? new GPSModel();
+ }
public static OperadoresService Operadores { get; set; } = new OperadoresService();
diff --git a/AgroBase/AgroBase/Services/GPSService.cs b/AgroBase/AgroBase/Services/GPSService.cs
index e8bf59744..fe02a924e 100644
--- a/AgroBase/AgroBase/Services/GPSService.cs
+++ b/AgroBase/AgroBase/Services/GPSService.cs
@@ -1,6 +1,7 @@
-using AgroBase.Models;
+using AgroBase.Models;
using Newtonsoft.Json;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -9,30 +10,41 @@ using System.IO.Ports;
using System.Linq;
using System.Net.Sockets;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using static AgroBase.Models.Enums;
namespace AgroBase.Services
{
+ ///
+ /// Serviço GNSS/RTCM do rover.
+ ///
+ /// Princípios:
+ /// - leitura serial nunca executa rede, disco ou tarefas lentas;
+ /// - toda escrita no UM982 passa por um único lock;
+ /// - RTCM entra em fila limitada e mensagens velhas são descartadas;
+ /// - NTRIP e MQTT compartilham o mesmo trilho de escrita;
+ /// - intervalos e idades usam relógio monotônico;
+ /// - configuração do módulo é exclusiva;
+ /// - mantém as assinaturas públicas usadas pelo projeto legado.
+ ///
public class GPSService
{
+ // ============================================================
+ // CONTRATO PÚBLICO LEGADO
+ // ============================================================
+
public static SerialPort PortaGPS = null;
+
+ ///
+ /// Estado puro. Consultar esta propriedade não abre nem altera a porta.
+ ///
public static bool Iniciado
{
get
{
- if (PortaGPS != null && !PortaGPS.IsOpen)
- {
- try
- {
- PortaGPS.Open();
- }
- catch
- {
- PortaGPS = null;
- }
- }
- return PortaGPS != null && PortaGPS.IsOpen;
+ SerialPort porta = PortaGPS;
+ return porta != null && porta.IsOpen;
}
}
@@ -46,1079 +58,2062 @@ namespace AgroBase.Services
private static bool InverterHeading = true;
private static int rtk_timeout = 60;
private static int TempoMin_Ntrip = 10;
- private static bool LoopRTK_Ntrip = false;
- public static bool CorrecaoRTK_Ntrip = false;
+
+ private static volatile bool LoopRTK_Ntrip = false;
+ public static volatile bool CorrecaoRTK_Ntrip = false;
+
+ ///
+ /// Mantido para compatibilidade e diagnóstico humano.
+ /// Watchdogs internos usam Stopwatch.
+ ///
public static DateTime UltimoEnvioCorrecaoRTK = DateTime.MinValue;
- public static GeoLeverArm LeverArm = new GeoLeverArm(offsetFisicoFrontalCm: VariaveisEquipamento.LeverArmFrontalCm, offsetFisicoLateralCm: VariaveisEquipamento.LeverArmLateralCm);
- public static void AtualizarPortaCOM(SerialPort Porta)
+ public static GeoLeverArm LeverArm = new GeoLeverArm(
+ offsetFisicoFrontalCm: VariaveisEquipamento.LeverArmFrontalCm,
+ offsetFisicoLateralCm: VariaveisEquipamento.LeverArmLateralCm
+ );
+
+ public static Queue historicoPosicao = new Queue();
+
+ // ============================================================
+ // SINCRONIZAÇÃO E CICLO DE VIDA
+ // ============================================================
+
+ private static readonly object _stateLock = new object();
+ private static readonly object _bufferLock = new object();
+ private static readonly object _rtcmQueueLock = new object();
+ private static readonly object _workerLock = new object();
+ private static readonly object _portLifecycleLock = new object();
+
+ private static readonly SemaphoreSlim _serialWriteLock =
+ new SemaphoreSlim(1, 1);
+
+ private static readonly SemaphoreSlim _configurationLock =
+ new SemaphoreSlim(1, 1);
+
+ private static readonly SemaphoreSlim _rtcmSignal =
+ new SemaphoreSlim(0, int.MaxValue);
+
+ private static readonly SemaphoreSlim _logSignal =
+ new SemaphoreSlim(0, int.MaxValue);
+
+ private static readonly SemaphoreSlim _localPublishSignal =
+ new SemaphoreSlim(0, int.MaxValue);
+
+ private static CancellationTokenSource _serviceCts;
+ private static Task _nmeaWorkerTask;
+ private static Task _rtcmWriterTask;
+ private static Task _logWriterTask;
+ private static Task _localPublisherTask;
+
+ private static CancellationTokenSource _configurationCts;
+
+ private static CancellationTokenSource _ntripCts;
+ private static Task _ntripTask;
+ private static TcpClient _ntripClient;
+
+ private static long _portGeneration;
+
+ // ============================================================
+ // BUFFERS E FILAS
+ // ============================================================
+
+ private static readonly StringBuilder _nmeaBuffer =
+ new StringBuilder(8192);
+
+ private const int MaxNmeaBufferChars = 64 * 1024;
+
+ private static readonly object _nmeaQueueLock = new object();
+ private static readonly Queue _nmeaQueue =
+ new Queue();
+ private static readonly SemaphoreSlim _nmeaSignal =
+ new SemaphoreSlim(0, int.MaxValue);
+ private const int NmeaQueueCapacity = 512;
+
+ private static readonly Queue _rtcmQueue =
+ new Queue();
+
+ private const int RtcmQueueCapacity = 48;
+ private const int RtcmMaxAgeMs = 2500;
+ private const int RtcmMaxPayloadBytes = 64 * 1024;
+
+ private static long _rtcmSequence;
+
+ private static readonly ConcurrentQueue _gpsLogQueue =
+ new ConcurrentQueue();
+
+ private static readonly object _localPublishLock = new object();
+ private static readonly Dictionary _localLatestPayload =
+ new Dictionary(StringComparer.Ordinal);
+
+ // ============================================================
+ // MÉTRICAS
+ // ============================================================
+
+ private static long _lastSerialRxMono;
+ private static long _lastValidNmeaMono;
+ private static long _lastValidGgaMono;
+ private static long _lastValidThsMono;
+ private static long _lastRtcmReceivedMono;
+ private static long _lastRtcmWrittenMono;
+
+ private static long _serialBytesReceived;
+ private static long _nmeaValid;
+ private static long _nmeaInvalid;
+ private static long _nmeaChecksumErrors;
+ private static long _nmeaBufferResets;
+ private static long _nmeaQueued;
+ private static long _nmeaDroppedQueue;
+
+ private static long _rtcmReceived;
+ private static long _rtcmQueued;
+ private static long _rtcmWritten;
+ private static long _rtcmDroppedQueue;
+ private static long _rtcmDroppedStale;
+ private static long _rtcmDroppedInvalid;
+ private static long _rtcmWriteErrors;
+
+ private static long _configurationRuns;
+ private static long _configurationErrors;
+
+ private static long _ntripReconnects;
+ private static long _ntripBytesReceived;
+ private static long _ntripErrors;
+
+ private static string _lastError;
+ private static readonly object _metricsTextLock = new object();
+
+ // ============================================================
+ // INICIALIZAÇÃO DA PORTA
+ // ============================================================
+
+ public static void AtualizarPortaCOM(SerialPort porta)
{
- if (PortaGPS == null)
- {
- PortaGPS = new SerialPort();
- }
- else if (Iniciado)
- {
- PortaGPS.Close();
- }
+ if (porta == null)
+ throw new ArgumentNullException(nameof(porta));
- PortaGPS.BaudRate = Porta.BaudRate;
- PortaGPS.PortName = Porta.PortName;
- PortaGPS.ReadTimeout = 2000; // Timeout de 2 segundos
- PortaGPS.WriteTimeout = 2000; // Timeout de 2 segundos
- PortaGPS.DataReceived -= PortaGPS_DataReceived;
- PortaGPS.DataReceived += PortaGPS_DataReceived;
+ EnsureBackgroundWorkersStarted();
- Porta.Close();
+ long generation = Interlocked.Increment(ref _portGeneration);
- if (Iniciado)
+ /*
+ * A troca da porta participa do mesmo trilho de escrita.
+ * Assim nenhum RTCM ou comando ASCII escreve enquanto a
+ * SerialPort está sendo fechada e substituída.
+ */
+ _serialWriteLock.Wait();
+
+ try
{
- DefinirDispositivo();
- Task.Run(async () => await ConfigurarModulo());
- if (!Variaveis.IsAgroMonitor && CorrecaoRTK_Ntrip)
+ lock (_portLifecycleLock)
{
- Task.Run(async () => await AplicarCorrecaoRTK_Ntrip());
+ SerialPort anterior = PortaGPS;
+
+ if (anterior != null)
+ {
+ try
+ {
+ anterior.DataReceived -= PortaGPS_DataReceived;
+ }
+ catch { }
+
+ try
+ {
+ if (anterior.IsOpen)
+ anterior.Close();
+ }
+ catch (Exception ex)
+ {
+ RecordError(
+ "[GPSService.AtualizarPortaCOM] Erro ao fechar porta antiga: " +
+ ex.Message
+ );
+ }
+
+ try
+ {
+ anterior.Dispose();
+ }
+ catch { }
+ }
+
+ var novaPorta = new SerialPort
+ {
+ BaudRate = porta.BaudRate,
+ PortName = porta.PortName,
+ ReadTimeout = 2000,
+ WriteTimeout = 2000,
+ Encoding = Encoding.ASCII,
+ DtrEnable = false,
+ RtsEnable = false
+ };
+
+ novaPorta.DataReceived += PortaGPS_DataReceived;
+
+ try
+ {
+ if (porta.IsOpen)
+ porta.Close();
+ }
+ catch { }
+
+ PortaGPS = novaPorta;
+
+ try
+ {
+ novaPorta.Open();
+ }
+ catch (Exception ex)
+ {
+ novaPorta.DataReceived -= PortaGPS_DataReceived;
+ PortaGPS = null;
+ RecordError(
+ "[GPSService.AtualizarPortaCOM] Não foi possível abrir " +
+ novaPorta.PortName + ": " + ex.Message
+ );
+ return;
+ }
}
}
+ finally
+ {
+ _serialWriteLock.Release();
+ }
+
+ DefinirDispositivo();
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await ConfigurarModulo().ConfigureAwait(false);
+
+ if (generation != Interlocked.Read(ref _portGeneration))
+ return;
+
+ if (!Variaveis.IsAgroMonitor && CorrecaoRTK_Ntrip)
+ await IniciarNtripAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ RecordError(
+ "[GPSService.AtualizarPortaCOM] Falha pós-conexão: " +
+ ex.Message
+ );
+ }
+ });
}
private static void DefinirDispositivo()
{
- if (Iniciado)
+ SerialPort porta = PortaGPS;
+
+ if (porta == null || !porta.IsOpen)
+ return;
+
+ try
{
- SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel()
+ bool jaExiste = SerialService.DispositivosMapeados.Any(
+ x => x.Dispositivo == T_Code.Gps &&
+ string.Equals(
+ x.Endereco,
+ porta.PortName,
+ StringComparison.OrdinalIgnoreCase
+ )
+ );
+
+ if (!jaExiste)
{
- Dispositivo = T_Code.Gps,
- Endereco = PortaGPS.PortName,
- Versao = "1",
- });
+ SerialService.DispositivosMapeados.Add(
+ new DispositivoDetalhesModel
+ {
+ Dispositivo = T_Code.Gps,
+ Endereco = porta.PortName,
+ Versao = "1"
+ }
+ );
+ }
+ }
+ catch (Exception ex)
+ {
+ RecordError(
+ "[GPSService.DefinirDispositivo] " + ex.Message
+ );
}
}
+ // ============================================================
+ // CONFIGURAÇÃO DO UM982
+ // ============================================================
+
public static async Task ConfigurarModulo()
{
- Variaveis.MostrarLog($"[GPSService.ConfigurarModulo] Iniciando configuração do módulo GPS...");
- await ConfigurarModuloRover(comprimento_antena: 100);
- }
+ EnsureBackgroundWorkersStarted();
- private static async Task ConfigurarModuloRover(string porta_usb = "com3", string porta_entrada = "com2", int comprimento_antena = 100, int tolerancia_antena = 5)
- {
- string freq = (1.0 / TaxaAmostragemHz).ToString("0.0").Replace(",", ".");
- string[] comandos = {
- // Bauds
- $"config com1 115200\r\n",
- $"config com2 115200\r\n",
- $"config com3 115200\r\n",
+ CancellationTokenSource novaCts = new CancellationTokenSource();
+ CancellationTokenSource anterior =
+ Interlocked.Exchange(ref _configurationCts, novaCts);
- // limpa logs das portas
- $"unlog com1\r\n",
- $"unlog com2\r\n",
- $"unlog com3\r\n",
-
- // modo rover + RTK
- $"mode rover uav\r\n",
-
- // timeouts de correção
- $"config rtk timeout {rtk_timeout}\r\n",
- $"config dgps timeout 60\r\n", // ou 0 para desabilitar DGPS fallback
-
- // heading 2 antenas
- $"config heading fixlength\r\n",
- //$"config heading tractor\r\n",
- $"config heading length {comprimento_antena} {tolerancia_antena}\r\n",
- // (se precisar, existe 'config heading offset ')
-
- // NMEA só na USB (COM1)
- $"gngga {porta_usb} {freq}\r\n",
- $"gpths {porta_usb} {freq}\r\n",
- $"gpvtg {porta_usb} {freq}\r\n",
-
- $"saveconfig\r\n"
- };
-
- await Task.Delay(2000);
-
- foreach (string comando in comandos)
+ if (anterior != null)
{
- try
- {
- byte[] bytesComando = Encoding.ASCII.GetBytes(comando);
- PortaGPS?.Write(bytesComando, 0, bytesComando.Length);
- }
- catch (Exception ex)
- {
- Variaveis.MostrarLog($"[GPSService.ConfigurarModuloRover] Erro ao enviar comando de configuração GNSS: {ex.Message}");
- }
- await Task.Delay(1000); // Pequeno delay para evitar sobrecarga na comunicação
+ try { anterior.Cancel(); } catch { }
+ anterior.Dispose();
}
+
+ await ConfigurarModuloRover(
+ comprimento_antena: 100,
+ cancellationToken: novaCts.Token
+ ).ConfigureAwait(false);
}
- private static async Task ConfigurarModuloBase(string porta_usb = "com3", string porta_saida = "com2", int tempo_fixacao = 60)
+ private static async Task ConfigurarModuloRover(
+ string porta_usb = "com3",
+ string porta_entrada = "com2",
+ int comprimento_antena = 100,
+ int tolerancia_antena = 5,
+ CancellationToken cancellationToken = default(CancellationToken))
{
- string base_id = "957";
- string distancia_min = "0";
- string[] comandos = {
- // Bauds
- $"config com1 115200\r\n",
- $"config com2 115200\r\n",
- $"config com3 115200\r\n",
+ await _configurationLock.WaitAsync(cancellationToken)
+ .ConfigureAwait(false);
- // limpa logs das portas
- $"unlog com1\r\n",
- $"unlog com2\r\n",
- $"unlog com3\r\n",
+ Interlocked.Increment(ref _configurationRuns);
- // Base com Survey-In (tempo + acurácia)
- $"mode base {base_id} time {tempo_fixacao} {distancia_min}\r\n",
-
- // RTCM perfil (comece leve; ative mais constelações se o LoRa aguentar)
- $"RTCM1006 {porta_saida} 10\r\n",
- $"RTCM1033 {porta_saida} 30\r\n",
- $"RTCM1074 {porta_saida} 1\r\n", // GPS MSM4
- $"RTCM1124 {porta_saida} 1\r\n", // BeiDou MSM4
-
- // (Opcional) ativar mais constelações:
- $"RTCM1094 {porta_saida} 1\r\n", // Galileo MSM4
- $"RTCM1084 {porta_saida} 1\r\n", // GLONASS MSM4
- //$"RTCM1230 {porta_saida} 10\r\n",// Bias GLONASS (OBRIGATÓRIO se 1084 estiver ativo)
-
- // NMEA mínimo para debug na USB
- $"gngga {porta_usb} 1\r\n",
-
- $"saveconfig\r\n",
- };
-
- await Task.Delay(2000);
-
- foreach (string cmd in comandos)
- {
- try
- {
- byte[] bytes = Encoding.ASCII.GetBytes(cmd);
- PortaGPS.Write(bytes, 0, bytes.Length);
- }
- catch (Exception ex)
- {
- Variaveis.MostrarLog($"[GPSService.ConfigurarModuloBase] Erro ao enviar comando de configuração GNSS: {ex.Message}");
- }
- await Task.Delay(500);
- }
- }
-
-
-
- private static readonly object _bufferLock = new object();
- private static readonly StringBuilder _buffer = new StringBuilder();
-
- private static void PortaGPS_DataReceived(object sender, SerialDataReceivedEventArgs e)
- {
try
{
- if (!(PortaGPS?.IsOpen ?? false))
- return;
+ Variaveis.MostrarLog(
+ "[GPSService.ConfigurarModuloRover] Iniciando configuração."
+ );
- string recebido = PortaGPS.ReadExisting();
+ string freq = (1.0 / Math.Max(1, TaxaAmostragemHz))
+ .ToString("0.0", CultureInfo.InvariantCulture);
+
+ string[] comandos =
+ {
+ "config com1 115200\r\n",
+ "config com2 115200\r\n",
+ "config com3 115200\r\n",
+
+ "unlog com1\r\n",
+ "unlog com2\r\n",
+ "unlog com3\r\n",
+
+ "mode rover uav\r\n",
+
+ "config rtk timeout " + rtk_timeout + "\r\n",
+ "config dgps timeout 60\r\n",
+
+ "config heading fixlength\r\n",
+ "config heading length " +
+ comprimento_antena + " " +
+ tolerancia_antena + "\r\n",
+
+ "gngga " + porta_usb + " " + freq + "\r\n",
+ "gpths " + porta_usb + " " + freq + "\r\n",
+ "gpvtg " + porta_usb + " " + freq + "\r\n",
+
+ "saveconfig\r\n"
+ };
+
+ await Task.Delay(1500, cancellationToken)
+ .ConfigureAwait(false);
+
+ /*
+ * Mantém exclusividade durante a sequência completa.
+ * RTCMs que envelhecerem enquanto o módulo é configurado
+ * serão descartados pelo worker, em vez de serem injetados
+ * entre comandos ASCII.
+ */
+ await _serialWriteLock.WaitAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ try
+ {
+ foreach (string comando in comandos)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ WriteSerialUnsafe(
+ Encoding.ASCII.GetBytes(comando),
+ comando.Length
+ );
+
+ await Task.Delay(350, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ _serialWriteLock.Release();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Interlocked.Increment(ref _configurationErrors);
+ RecordError(
+ "[GPSService.ConfigurarModuloRover] " + ex.Message
+ );
+ throw;
+ }
+ finally
+ {
+ _configurationLock.Release();
+ }
+ }
+
+ private static async Task ConfigurarModuloBase(
+ string porta_usb = "com3",
+ string porta_saida = "com2",
+ int tempo_fixacao = 60,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ await _configurationLock.WaitAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ Interlocked.Increment(ref _configurationRuns);
+
+ try
+ {
+ string[] comandos =
+ {
+ "config com1 115200\r\n",
+ "config com2 115200\r\n",
+ "config com3 115200\r\n",
+
+ "unlog com1\r\n",
+ "unlog com2\r\n",
+ "unlog com3\r\n",
+
+ "mode base 957 time " + tempo_fixacao + " 0\r\n",
+
+ "RTCM1006 " + porta_saida + " 10\r\n",
+ "RTCM1033 " + porta_saida + " 30\r\n",
+ "RTCM1074 " + porta_saida + " 1\r\n",
+ "RTCM1084 " + porta_saida + " 1\r\n",
+ "RTCM1094 " + porta_saida + " 1\r\n",
+ "RTCM1124 " + porta_saida + " 1\r\n",
+ "RTCM1230 " + porta_saida + " 10\r\n",
+
+ "gngga " + porta_usb + " 1\r\n",
+ "saveconfig\r\n"
+ };
+
+ await _serialWriteLock.WaitAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ try
+ {
+ foreach (string comando in comandos)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ byte[] bytes = Encoding.ASCII.GetBytes(comando);
+ WriteSerialUnsafe(bytes, bytes.Length);
+
+ await Task.Delay(350, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ _serialWriteLock.Release();
+ }
+ }
+ catch
+ {
+ Interlocked.Increment(ref _configurationErrors);
+ throw;
+ }
+ finally
+ {
+ _configurationLock.Release();
+ }
+ }
+
+ // ============================================================
+ // RECEPÇÃO SERIAL / NMEA
+ // ============================================================
+
+ private static void PortaGPS_DataReceived(
+ object sender,
+ SerialDataReceivedEventArgs e)
+ {
+ SerialPort porta = sender as SerialPort;
+
+ if (porta == null || !porta.IsOpen)
+ return;
+
+ try
+ {
+ string recebido = porta.ReadExisting();
if (string.IsNullOrEmpty(recebido))
return;
- string blocoCompleto;
-
- lock (_bufferLock)
- {
- _buffer.Append(recebido);
-
- string acumulado = _buffer.ToString();
- int ultimaQuebraLinha = acumulado.LastIndexOf('\n');
-
- // Ainda não chegou nenhuma sentença completa.
- if (ultimaQuebraLinha < 0)
- return;
-
- blocoCompleto = acumulado.Substring(
- 0,
- ultimaQuebraLinha + 1
- );
-
- string restante = acumulado.Substring(
- ultimaQuebraLinha + 1
- );
-
- /*
- * Retiramos as sentenças completas do buffer antes
- * de processá-las. Assim, uma exceção em qualquer
- * consumidor não reapresenta as mesmas sentenças.
- */
- _buffer.Clear();
- _buffer.Append(restante);
- }
-
- string[] mensagens = blocoCompleto.Split('\n');
-
- foreach (string mensagemRaw in mensagens)
- {
- string mensagem = mensagemRaw.Trim();
-
- if (mensagem.Length == 0)
- continue;
-
- try
- {
- ProcessarDadosNMEA(mensagem);
- }
- catch (Exception ex)
- {
- Variaveis.MostrarLog(
- $"[GPSService.PortaGPS_DataReceived] " +
- $"Erro ao processar a sentença '{mensagem}': {ex}"
- );
- }
- }
-
- var obj = UltimaLeitura.Clone();
- obj.Momento = DateTime.Now;
-
- Logs.Add(JsonConvert.SerializeObject(obj));
-
- VariaveisOperacao.RegistrarLogDispositivo(
- Logs,
- T_Code.Gps,
- ".json",
- 300
+ Interlocked.Add(
+ ref _serialBytesReceived,
+ Encoding.ASCII.GetByteCount(recebido)
);
+
+ Interlocked.Exchange(
+ ref _lastSerialRxMono,
+ Stopwatch.GetTimestamp()
+ );
+
+ List sentencas = ExtrairSentencasCompletas(recebido);
+
+ foreach (string sentenca in sentencas)
+ {
+ EnfileirarSentencaNmea(sentenca);
+ }
}
catch (Exception ex)
{
- Variaveis.MostrarLog(
- $"[GPSService.PortaGPS_DataReceived] " +
- $"Erro geral na porta serial: {ex}"
+ RecordError(
+ "[GPSService.PortaGPS_DataReceived] " + ex.Message
);
}
}
-
-
- private static void ProcessarDadosNMEA(string nmeaData)
+ private static void EnfileirarSentencaNmea(string sentenca)
{
- DateTime Agora = DateTime.Now;
- var linhas = nmeaData.Split('\n');
- foreach (var linha in linhas)
+ if (string.IsNullOrWhiteSpace(sentenca))
+ return;
+
+ lock (_nmeaQueueLock)
{
- if (string.IsNullOrWhiteSpace(linha)) continue;
+ while (_nmeaQueue.Count >= NmeaQueueCapacity)
+ {
+ _nmeaQueue.Dequeue();
+ Interlocked.Increment(ref _nmeaDroppedQueue);
+ }
- var sentenca = linha.Trim();
- //Console.WriteLine(sentenca);
-
- // Identifica o tipo de sentença
-
- // GPS Antigo
- if (sentenca.StartsWith("$GPGGA"))
- {
- ProcessarGPGGA(sentenca);
- AtualizarCoordenadasGPS();
- }
- // Coordenadas
- else if (sentenca.StartsWith("$GNGGA") || sentenca.StartsWith("$GLGGA"))
- {
- ProcessarGNGGA(sentenca);
- AtualizarCoordenadasGPS();
- }
- // Variação magnética
- else if (sentenca.StartsWith("$GNRMC"))
- {
- ProcessarGNRMC(sentenca);
- }
- // Curso verdadeiro
- else if (sentenca.StartsWith("$GNVTG") || sentenca.StartsWith("$GPVTG"))
- {
- ProcessarGNVTG(sentenca);
- }
- // Satélites em vista
- else if (sentenca.StartsWith("$GPGSV") || sentenca.StartsWith("$GLGSV") || sentenca.StartsWith("$GBGSV") || sentenca.StartsWith("$GAGSV"))
- {
- ProcessarGSV(sentenca);
- }
- // Orientação real
- else if (sentenca.StartsWith("$GNTHS") || sentenca.StartsWith("$GPTHS") || sentenca.StartsWith("$GATHS"))
- {
- ProcessarGNTHS(sentenca);
- //AtualizarCoordenadasGPS();
- }
- // GLL (GP/GN/GL/GA/BD)
- else if (sentenca.Length > 6 && sentenca[3] == 'G' && sentenca[4] == 'L' && sentenca[5] == 'L')
- {
- //ProcessarGNGLL(sentenca);
- }
- // GSA (GP/GN/GL/GA/BD)
- else if (sentenca.Length > 6 && sentenca[3] == 'G' && sentenca[4] == 'S' && sentenca[5] == 'A')
- {
- ProcessarGxGSA(sentenca);
- }
- else if (sentenca.Length > 6 && sentenca[3] == 'R' && sentenca[4] == 'M' && sentenca[5] == 'C') // RMC
- {
- ProcessarGxRMC(sentenca);
- }
- // ACK de comando do UM982, não é sentença NMEA de navegação.
- else if (sentenca.StartsWith("$command,", StringComparison.OrdinalIgnoreCase))
- {
- Variaveis.MostrarLog($"[GPSService.ProcessarDadosNMEA] {sentenca}");
- return;
- }
- else
- {
- Variaveis.MostrarLog($"[GPSService.ProcessarDadosNMEA] Sentença desconhecida: {sentenca}");
- }
+ _nmeaQueue.Enqueue(sentenca);
+ Interlocked.Increment(ref _nmeaQueued);
}
- PenultimaLeitura.UltimoComandoRespondido = UltimaLeitura.UltimoComandoRespondido;
- UltimaLeitura.UltimoComandoRespondido = Agora;
+ try { _nmeaSignal.Release(); }
+ catch (SemaphoreFullException) { }
}
- private static void ProcessarGPGGA(string sentenca)
+ private static async Task NmeaWorkerLoopAsync(
+ CancellationToken cancellationToken)
{
- string[] parts = sentenca.Split(',');
-
- PenultimaLeitura.Momento = UltimaLeitura.Momento;
- PenultimaLeitura.TimestampPos = UltimaLeitura.TimestampPos.Clone();
- PenultimaLeitura.Latitude = UltimaLeitura.Latitude;
- PenultimaLeitura.Longitude = UltimaLeitura.Longitude;
- PenultimaLeitura.LatitudeAnt = UltimaLeitura.LatitudeAnt;
- PenultimaLeitura.LongitudeAnt = UltimaLeitura.LongitudeAnt;
- PenultimaLeitura.Altitude = UltimaLeitura.Altitude;
- PenultimaLeitura.PrecisaoHorizontal = UltimaLeitura.PrecisaoHorizontal;
- PenultimaLeitura.DataHora = UltimaLeitura.DataHora;
- PenultimaLeitura.NumeroSatelites = UltimaLeitura.NumeroSatelites;
-
- UltimaLeitura.TimestampPos.valor = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
- UltimaLeitura.Momento = DateTime.Now;
-
- double? lat = null;
- double? lon = null;
- if (parts[2] != "" && parts[3] != "")
+ while (!cancellationToken.IsCancellationRequested)
{
- lat = GPSUtils.ConvertToDecimalDegrees(parts[2], parts[3], 2);
- }
- if (parts[4] != "" && parts[5] != "")
- {
- lon = GPSUtils.ConvertToDecimalDegrees(parts[4], parts[5], 3);
- }
- if (lat != null && lon != null)
- {
- UltimaLeitura.LatitudeAnt = lat.Value;
- UltimaLeitura.LongitudeAnt = lon.Value;
- AplicarPosicaoCorrigida();
- }
- UltimaLeitura.NumeroSatelites = int.TryParse(parts[7], out int numSat) ? numSat : 0;
- UltimaLeitura.PrecisaoHorizontal = double.TryParse(parts[8], NumberStyles.Float, CultureInfo.InvariantCulture, out double hdop) ? hdop : 0;
- UltimaLeitura.Altitude = double.TryParse(parts[9], NumberStyles.Float, CultureInfo.InvariantCulture, out double alt) ? alt : 0;
- }
-
- private static void ProcessarGNGGA(string sentenca)
- {
- var ci = CultureInfo.InvariantCulture;
- var campos = sentenca.Split(',');
-
- string horaUTC = campos.Length > 1 ? campos[1] : "";
- string latitudeRaw = campos.Length > 2 ? campos[2] : "";
- string hemisferioLat = campos.Length > 3 ? campos[3] : "";
- string longitudeRaw = campos.Length > 4 ? campos[4] : "";
- string hemisferioLon = campos.Length > 5 ? campos[5] : "";
- string qualidade = campos.Length > 6 ? campos[6] : "0";
- string satelitesUsados = campos.Length > 7 ? campos[7] : "0";
- string hdop = campos.Length > 8 ? campos[8] : "99.9";
- string altitudeRaw = campos.Length > 9 ? campos[9] : "0";
- string geoidSepRaw = campos.Length > 11 ? campos[11] : "0";
- string idadeCorrecaoRaw = campos.Length > 13 ? campos[13] : "";
- string base_id = campos.Length > 14 ? campos[14].Split('*')[0] : "";
-
- // Conversão de Latitude (ddmm.mmmm)
- double latitude = 0;
- if (!string.IsNullOrEmpty(latitudeRaw))
- {
- // lat tem 2 dígitos de graus
- var deg = double.Parse(latitudeRaw.Substring(0, 2), ci);
- var min = double.Parse(latitudeRaw.Substring(2), ci);
- latitude = deg + (min / 60.0);
- if (hemisferioLat.Equals("S", StringComparison.OrdinalIgnoreCase)) latitude *= -1;
- }
-
-
- // Conversão de Longitude (dddmm.mmmm)
- double longitude = 0;
- if (!string.IsNullOrEmpty(longitudeRaw))
- {
- // lon tem 3 dígitos de graus
- var deg = double.Parse(longitudeRaw.Substring(0, 3), ci);
- var min = double.Parse(longitudeRaw.Substring(3), ci);
- longitude = deg + (min / 60.0);
- if (hemisferioLon.Equals("W", StringComparison.OrdinalIgnoreCase)) longitude *= -1;
- }
-
- // Altitude MSL (campo 9)
- double altMSL = 0;
- double.TryParse(altitudeRaw, NumberStyles.Float, ci, out altMSL);
-
- // Geoid separation (campo 11)
- double geoidSep = 0;
- double.TryParse(geoidSepRaw, NumberStyles.Float, ci, out geoidSep);
-
- // Altura elipsoidal = MSL + geoid separation
- double altElipsoidal = altMSL + geoidSep;
-
- // HDOP (adimensional)
- double.TryParse(hdop, NumberStyles.Float, ci, out double hdopVal);
-
- int.TryParse(satelitesUsados, out int nsatelites);
- int.TryParse(qualidade, out int fixCode);
- double idadeCorrecao = -1;
- if (!string.IsNullOrWhiteSpace(idadeCorrecaoRaw))
- double.TryParse(idadeCorrecaoRaw, NumberStyles.Float, ci, out idadeCorrecao);
-
-
- //Console.WriteLine($"GNGGA: Hora={horaUTC}, Latitude={latitude}, Longitude={longitude}, Qualidade={qualidade}, Satélites={satelitesUsados}, HDOP={hdop}, Altitude={altitude}");
-
- PenultimaLeitura.TimestampPos = UltimaLeitura.TimestampPos.Clone();
- PenultimaLeitura.Momento = UltimaLeitura.Momento;
- PenultimaLeitura.Latitude = UltimaLeitura.Latitude;
- PenultimaLeitura.Longitude = UltimaLeitura.Longitude;
- PenultimaLeitura.LatitudeAnt = UltimaLeitura.LatitudeAnt;
- PenultimaLeitura.LongitudeAnt = UltimaLeitura.LongitudeAnt;
- PenultimaLeitura.Altitude = UltimaLeitura.Altitude;
- PenultimaLeitura.AltitudeElipsoidal = UltimaLeitura.AltitudeElipsoidal;
- PenultimaLeitura.PrecisaoHorizontal = UltimaLeitura.PrecisaoHorizontal;
- PenultimaLeitura.NumeroSatelites = UltimaLeitura.NumeroSatelites;
- PenultimaLeitura.QualidadeFix = UltimaLeitura.QualidadeFix;
- PenultimaLeitura.DataHora = UltimaLeitura.DataHora;
- PenultimaLeitura.IdadeCorrecao = UltimaLeitura.IdadeCorrecao;
- PenultimaLeitura.BaseID = UltimaLeitura.BaseID;
-
- // Armazenar os valores na última leitura
- UltimaLeitura.TimestampPos.valor = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
- UltimaLeitura.Momento = DateTime.Now;
- UltimaLeitura.LatitudeAnt = latitude;
- UltimaLeitura.LongitudeAnt = longitude;
- UltimaLeitura.Altitude = altMSL;
- UltimaLeitura.AltitudeElipsoidal = altElipsoidal;
- UltimaLeitura.PrecisaoHorizontal = hdopVal;
- UltimaLeitura.NumeroSatelites = nsatelites;
- UltimaLeitura.QualidadeFix = (TiposCorrecaoGPS)fixCode;
- UltimaLeitura.IdadeCorrecao = idadeCorrecao;
- UltimaLeitura.BaseID = base_id;
-
- AplicarPosicaoCorrigida();
-
- // Hora UTC no formato HHmmss.ss (fração opcional)
- if (!string.IsNullOrEmpty(horaUTC) && horaUTC.Length >= 6)
- {
- // Pega HHmmss e, se houver, fração:
- var hh = int.Parse(horaUTC.Substring(0, 2), ci);
- var mm = int.Parse(horaUTC.Substring(2, 2), ci);
- var ssStr = horaUTC.Substring(4); // "ss" ou "ss.ss"
- double ss = double.Parse(ssStr, ci);
- var ts = new TimeSpan(0, hh, mm, (int)Math.Floor(ss), (int)Math.Round((ss - Math.Floor(ss)) * 1000.0));
- var currentDateUtc = DateTime.UtcNow.Date;
- UltimaLeitura.DataHora = currentDateUtc.Add(ts).ToLocalTime();
- }
-
- // 1) define origem ENU na primeira leitura válida
- if (!UltimaLeitura.EnuOriginSet && UltimaLeitura.QualidadeFix == TiposCorrecaoGPS.RTKFixo) // tem fix
- {
- UltimaLeitura.Lat0 = latitude;
- UltimaLeitura.Lon0 = longitude;
- UltimaLeitura.EnuOriginSet = true;
- }
-
- }
-
- private static void ProcessarGNRMC(string sentenca)
- {
- var campos = sentenca.Split(',');
-
- string horaUTC = campos[1].Replace(".", ",");
- string status = campos[2].Replace(".", ",");
- string latitudeRaw = campos[3].Replace(".", ",");
- string hemisferioLat = campos[4].Replace(".", ",");
- string longitudeRaw = campos[5].Replace(".", ",");
- string hemisferioLon = campos[6].Replace(".", ",");
- string velocidadeSobreSolo = campos[7].Replace(".", ",");
- string curso = campos[8].Replace(".", ",");
- string data = campos[9].Replace(".", ",");
- string variaçãoMagnetica = campos[10].Replace(".", ",");
-
- double.TryParse(variaçãoMagnetica, out double variacaoMag);
-
- PenultimaLeitura.Momento = UltimaLeitura.Momento;
- PenultimaLeitura.VariacaoMagnetica = UltimaLeitura.VariacaoMagnetica;
-
- UltimaLeitura.Momento = DateTime.Now;
- UltimaLeitura.VariacaoMagnetica = variacaoMag;
-
- //Console.WriteLine($"GNRMC: Hora={horaUTC}, Status={status}, Latitude={latitudeRaw}{hemisferioLat}, Longitude={longitudeRaw}{hemisferioLon}, Velocidade={velocidadeSobreSolo}, Curso={curso}, Data={data}, Variação Magnética={variaçãoMagnetica}");
- }
-
- private static void ProcessarGNVTG(string sentenca)
- {
- var campos = sentenca.Split(',');
-
- string cursoVerdadeiro = campos[1].Replace(".", ",");
- string referenciaCurso = campos[2].Replace(".", ","); // T = Verdadeiro, M = Magnético
- string velocidadeSobreSoloKnots = campos[5].Replace(".", ",");
- string velocidadeSobreSoloKmh = campos[7].Replace(".", ",");
-
- //Console.WriteLine($"GNVTG: Curso Verdadeiro={cursoVerdadeiro}{referenciaCurso}, Velocidade (nós)={velocidadeSobreSoloKnots}, Velocidade (km/h)={velocidadeSobreSoloKmh}");
-
- PenultimaLeitura.Momento = UltimaLeitura.Momento;
- PenultimaLeitura.CursoVerdadeiro = UltimaLeitura.CursoVerdadeiro;
- PenultimaLeitura.Velocidade = UltimaLeitura.Velocidade;
-
- UltimaLeitura.Momento = DateTime.Now;
-
- double.TryParse(cursoVerdadeiro, out double curso);
- UltimaLeitura.CursoVerdadeiro = curso;
-
- double.TryParse(velocidadeSobreSoloKmh, out double velocidade);
- UltimaLeitura.Velocidade = velocidade;
- }
-
- private static void ProcessarGSV(string sentenca)
- {
- var campos = sentenca.Split(',');
-
- string tipoSistema = sentenca.Substring(1, 2); // GP = GPS, GL = GLONASS, etc.
- string totalSentencas = campos[1];
- string sentencaAtual = campos[2];
- string satelitesVisiveis = campos[3];
-
- PenultimaLeitura.Momento = UltimaLeitura.Momento;
- PenultimaLeitura.SatelitesEmVista = new List(UltimaLeitura.SatelitesEmVista);
-
-
- UltimaLeitura.Momento = DateTime.Now;
-
- int.TryParse(sentencaAtual, out int sentAtual);
- int.TryParse(totalSentencas, out int sentTotal);
- int.TryParse(satelitesVisiveis, out int visiveis);
-
- if (!UltimaLeitura.SatelitesEmVista.Any(x => x.TipoSistema == tipoSistema))
- {
- UltimaLeitura.SatelitesEmVista.Add(new GPSSatelitesEmVistaModel()
+ try
{
- TipoSistema = tipoSistema,
- Sentencas = new List()
- });
- }
-
- var leitura = UltimaLeitura.SatelitesEmVista.First(x => x.TipoSistema == tipoSistema);
-
- //Console.WriteLine($"GSV: Sistema={tipoSistema}, Sentença {sentencaAtual}/{totalSentencas}, Satélites Visíveis={satelitesVisiveis}");
-
- if (!leitura.Sentencas.Any(x => x.SentencaAtual == sentAtual))
- {
- leitura.Sentencas.Add(new GPSSatelitesEmVistaSentencaModel()
+ await _nmeaSignal.WaitAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
{
- SentencaAtual = sentAtual,
- SentencasTotal = sentTotal,
- QuantidadeSatelites = visiveis,
- Dados = new List()
- });
- }
+ break;
+ }
- var _sentenca = leitura.Sentencas.First(x => x.SentencaAtual == sentAtual);
+ bool processouAlgo = false;
- _sentenca.Dados = new List();
-
- for (int i = 4; i < campos.Length; i += 4)
- {
- if (i + 3 < campos.Length)
+ while (true)
{
- string prn = campos[i].Replace(".", ",");
- string elevacaoRaw = campos[i + 1].Replace(".", ",");
- string azimuteRaw = campos[i + 2].Replace(".", ",");
- string snrRaw = campos[i + 3].Replace(".", ",");
+ string sentenca = null;
- //Console.WriteLine($" Satélite PRN={prn}, Elevação={elevacaoRaw}, Azimute={azimuteRaw}, SNR={snrRaw}");
-
- double.TryParse(elevacaoRaw, out double elevacao);
- double.TryParse(azimuteRaw, out double azimute);
- double.TryParse(snrRaw, out double snr);
-
- _sentenca.Dados.Add(new GPSSatelitesEmVistaDadosModel()
+ lock (_nmeaQueueLock)
{
- PRN = prn,
- Elevacao = elevacao,
- Azimute = azimute,
- QualidadeSinal = snr
- });
+ if (_nmeaQueue.Count > 0)
+ sentenca = _nmeaQueue.Dequeue();
+ }
+
+ if (sentenca == null)
+ break;
+
+ ProcessarSentencaSegura(sentenca);
+ processouAlgo = true;
}
+
+ if (processouAlgo)
+ EnfileirarLogSnapshot();
}
}
- private static void ProcessarGNTHS(string sentenca)
+ private static List ExtrairSentencasCompletas(
+ string recebido)
+ {
+ var resultado = new List();
+
+ lock (_bufferLock)
+ {
+ _nmeaBuffer.Append(recebido);
+
+ if (_nmeaBuffer.Length > MaxNmeaBufferChars)
+ {
+ /*
+ * Tenta preservar a última possível sentença.
+ * Caso não exista '$', limpa completamente.
+ */
+ string acumuladoExcedido = _nmeaBuffer.ToString();
+ int ultimoInicio = acumuladoExcedido.LastIndexOf('$');
+
+ _nmeaBuffer.Clear();
+
+ if (ultimoInicio >= 0 &&
+ acumuladoExcedido.Length - ultimoInicio < 2048)
+ {
+ _nmeaBuffer.Append(
+ acumuladoExcedido.Substring(ultimoInicio)
+ );
+ }
+
+ Interlocked.Increment(ref _nmeaBufferResets);
+ }
+
+ while (true)
+ {
+ string acumulado = _nmeaBuffer.ToString();
+ int fim = acumulado.IndexOf('\n');
+
+ if (fim < 0)
+ break;
+
+ string linha = acumulado.Substring(0, fim)
+ .Trim('\r', '\n', ' ', '\t');
+
+ _nmeaBuffer.Remove(0, fim + 1);
+
+ if (!string.IsNullOrWhiteSpace(linha))
+ resultado.Add(linha);
+ }
+ }
+
+ return resultado;
+ }
+
+ private static void ProcessarSentencaSegura(string sentenca)
{
if (string.IsNullOrWhiteSpace(sentenca))
return;
try
{
- //var bicos = Variaveis.OperacaoEmAndamento.DispAtu?.Dados?.BicosPulverizadores?.Select(x => x.ComandoAtuar ? "ON" : "OFF").ToList();
- //string estadoSolenoides = bicos == null ? "INDISPONIVEL" : string.Join(",", bicos);
- //Variaveis.MostrarLog(
- // $"[GPSService.ProcessarGNTHS] " +
- // $"Sentença recebida: {sentenca}, " +
- // $"Solenoides: {estadoSolenoides}"
- //);
-
- /*
- * Exemplos:
- *
- * Válido:
- * $GPTHS,123.4567,A*XX
- *
- * Inválido:
- * $GPTHS,,V*0E
- */
-
- string conteudo = sentenca.Trim();
-
- if (conteudo.StartsWith("$"))
- conteudo = conteudo.Substring(1);
-
- string[] partesChecksum = conteudo.Split(
- new[] { '*' },
- 2
- );
-
- string corpo = partesChecksum[0];
-
- string[] campos = corpo.Split(',');
-
- // [0] = GPTHS/GNTHS
- // [1] = heading
- // [2] = status A/V
- if (campos.Length < 3)
+ if (!ValidarChecksumNmea(sentenca))
{
- Variaveis.MostrarLog(
- $"[GPSService.ProcessarGNTHS] Sentença incompleta: {sentenca}"
- );
+ Interlocked.Increment(ref _nmeaChecksumErrors);
+ Interlocked.Increment(ref _nmeaInvalid);
return;
}
- string headingTexto = (campos[1] ?? string.Empty).Trim();
-
- string statusRecebido = (campos[2] ?? string.Empty)
- .Trim()
- .ToUpperInvariant();
-
- bool headingNumerico = double.TryParse(
- headingTexto,
- NumberStyles.Float,
- CultureInfo.InvariantCulture,
- out double headingTrue
- );
-
- bool headingValido =
- statusRecebido == "A" &&
- headingNumerico &&
- !double.IsNaN(headingTrue) &&
- !double.IsInfinity(headingTrue);
-
- /*
- * Toda sentença recebida gera uma nova leitura,
- * mesmo quando o heading está inválido.
- *
- * Dessa forma, a frequência continua sendo calculada
- * pela chegada das sentenças THS.
- */
-
- PenultimaLeitura.TimestampOri.valor =
- UltimaLeitura.TimestampOri.valor;
-
- PenultimaLeitura.Momento =
- UltimaLeitura.Momento;
-
- PenultimaLeitura.OrientacaoReal =
- UltimaLeitura.OrientacaoReal;
-
- PenultimaLeitura.TipoOrientacao =
- UltimaLeitura.TipoOrientacao;
-
- UltimaLeitura.TimestampOri.valor =
- Stopwatch.GetTimestamp() /
- (double)Stopwatch.Frequency;
-
- UltimaLeitura.Momento = DateTime.Now;
-
- if (!headingValido)
+ lock (_stateLock)
{
- /*
- * Comunicação está funcionando, mas o UM982
- * não possui solução válida de heading.
- */
- UltimaLeitura.OrientacaoReal = 9999;
-
- UltimaLeitura.TipoOrientacao = "V";
-
- /*
- * Não aplicar posição corrigida nem atualizar
- * o ângulo usado pelo controle do rover.
- *
- * O valor 9999 existe apenas para telemetria
- * e diagnóstico de saúde.
- */
- return;
+ ProcessarDadosNMEA(sentenca);
}
- headingTrue = GPSUtils.NormalizarAngulo(
- headingTrue
+ Interlocked.Increment(ref _nmeaValid);
+ Interlocked.Exchange(
+ ref _lastValidNmeaMono,
+ Stopwatch.GetTimestamp()
);
-
- UltimaLeitura.OrientacaoReal =
- InverterHeading
- ? GPSUtils.NormalizarAngulo(
- headingTrue - 180.0
- )
- : headingTrue;
-
- UltimaLeitura.TipoOrientacao = "A";
-
- AplicarPosicaoCorrigida();
-
- // Atualiza o ângulo usado pelo restante do sistema
- // somente quando a solução é válida.
- DefinirAnguloCarro();
}
catch (Exception ex)
{
- Variaveis.MostrarLog(
- $"[GPSService.ProcessarGNTHS] " +
- $"Erro ao processar a sentença '{sentenca}': {ex.Message}"
+ Interlocked.Increment(ref _nmeaInvalid);
+ RecordError(
+ "[GPSService.ProcessarSentencaSegura] Sentença '" +
+ sentenca + "': " + ex.Message
);
}
}
- private static void ProcessarGNGLL(string sentenca)
+ private static bool ValidarChecksumNmea(string sentenca)
{
- // Aceita GP/GL/GN… qualquer “G?GLL”
- var campos = sentenca.Split(',');
- if (campos.Length < 7) return;
-
- // lat/lon
- string latRaw = campos[1];
- string latHem = campos[2];
- string lonRaw = campos[3];
- string lonHem = campos[4];
- string horaUTC = campos[5]; // hhmmss.ss
- string status = campos[6]; // A/V
- string mode = campos.Length > 7 ? campos[7].Split('*')[0] : ""; // pode não existir
-
- bool valido = status == "A" && mode != "N";
-
- if (valido && !string.IsNullOrWhiteSpace(latRaw) && !string.IsNullOrWhiteSpace(lonRaw))
+ if (sentenca.StartsWith(
+ "$command,",
+ StringComparison.OrdinalIgnoreCase))
{
- double lat = GPSUtils.DmmToDecimal(latRaw, 2);
- double lon = GPSUtils.DmmToDecimal(lonRaw, 3);
- if (!double.IsInfinity(lat) && !double.IsNaN(lat) && !double.IsInfinity(lon) && !double.IsNaN(lon))
+ return true;
+ }
+
+ if (!sentenca.StartsWith("$", StringComparison.Ordinal))
+ return false;
+
+ int asterisco = sentenca.LastIndexOf('*');
+
+ /*
+ * Alguns ACKs e firmwares podem não fornecer checksum.
+ * Sentenças de navegação sem '*' são aceitas por compatibilidade,
+ * mas as que possuem checksum são verificadas rigorosamente.
+ */
+ if (asterisco < 0)
+ return true;
+
+ if (asterisco + 2 >= sentenca.Length)
+ return false;
+
+ byte calculado = 0;
+
+ for (int i = 1; i < asterisco; i++)
+ calculado ^= (byte)sentenca[i];
+
+ byte informado;
+
+ return byte.TryParse(
+ sentenca.Substring(asterisco + 1, 2),
+ NumberStyles.HexNumber,
+ CultureInfo.InvariantCulture,
+ out informado
+ ) &&
+ calculado == informado;
+ }
+
+ private static void ProcessarDadosNMEA(string sentenca)
+ {
+ DateTime agora = DateTime.Now;
+
+ if (sentenca.StartsWith("$GPGGA"))
+ {
+ ProcessarGPGGA(sentenca);
+ AtualizarCoordenadasGPS();
+ MarkValidGga();
+ }
+ else if (sentenca.StartsWith("$GNGGA") ||
+ sentenca.StartsWith("$GLGGA"))
+ {
+ ProcessarGNGGA(sentenca);
+ AtualizarCoordenadasGPS();
+ MarkValidGga();
+ }
+ else if (sentenca.StartsWith("$GNRMC"))
+ {
+ ProcessarGxRMC(sentenca);
+ }
+ else if (sentenca.StartsWith("$GNVTG") ||
+ sentenca.StartsWith("$GPVTG"))
+ {
+ ProcessarGNVTG(sentenca);
+ }
+ else if (sentenca.StartsWith("$GPGSV") ||
+ sentenca.StartsWith("$GLGSV") ||
+ sentenca.StartsWith("$GBGSV") ||
+ sentenca.StartsWith("$GAGSV"))
+ {
+ ProcessarGSV(sentenca);
+ }
+ else if (sentenca.StartsWith("$GNTHS") ||
+ sentenca.StartsWith("$GPTHS") ||
+ sentenca.StartsWith("$GATHS"))
+ {
+ ProcessarGNTHS(sentenca);
+
+ Interlocked.Exchange(
+ ref _lastValidThsMono,
+ Stopwatch.GetTimestamp()
+ );
+ }
+ else if (sentenca.Length > 6 &&
+ sentenca[3] == 'G' &&
+ sentenca[4] == 'S' &&
+ sentenca[5] == 'A')
+ {
+ ProcessarGxGSA(sentenca);
+ }
+ else if (sentenca.Length > 6 &&
+ sentenca[3] == 'R' &&
+ sentenca[4] == 'M' &&
+ sentenca[5] == 'C')
+ {
+ ProcessarGxRMC(sentenca);
+ }
+ else if (sentenca.StartsWith(
+ "$command,",
+ StringComparison.OrdinalIgnoreCase))
+ {
+ Variaveis.MostrarLog(
+ "[GPSService.ProcessarDadosNMEA] " + sentenca
+ );
+ }
+
+ PenultimaLeitura.UltimoComandoRespondido =
+ UltimaLeitura.UltimoComandoRespondido;
+
+ UltimaLeitura.UltimoComandoRespondido = agora;
+ }
+
+ private static void MarkValidGga()
+ {
+ Interlocked.Exchange(
+ ref _lastValidGgaMono,
+ Stopwatch.GetTimestamp()
+ );
+ }
+
+ private static void ProcessarGPGGA(string sentenca)
+ {
+ string[] parts = sentenca.Split(',');
+
+ if (parts.Length < 10)
+ throw new FormatException("GPGGA incompleta.");
+
+ CopiarPosicaoParaPenultima();
+
+ UltimaLeitura.TimestampPos.valor =
+ Stopwatch.GetTimestamp() /
+ (double)Stopwatch.Frequency;
+
+ UltimaLeitura.Momento = DateTime.Now;
+
+ double lat;
+ double lon;
+
+ if (TryParseDmm(parts[2], parts[3], 2, out lat) &&
+ TryParseDmm(parts[4], parts[5], 3, out lon))
+ {
+ UltimaLeitura.LatitudeAnt = lat;
+ UltimaLeitura.LongitudeAnt = lon;
+ AplicarPosicaoCorrigida();
+ }
+
+ int numSat;
+ double hdop;
+ double alt;
+
+ UltimaLeitura.NumeroSatelites =
+ int.TryParse(parts[7], out numSat) ? numSat : 0;
+
+ UltimaLeitura.PrecisaoHorizontal =
+ double.TryParse(
+ parts[8],
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out hdop
+ ) ? hdop : 0;
+
+ UltimaLeitura.Altitude =
+ double.TryParse(
+ parts[9],
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out alt
+ ) ? alt : 0;
+ }
+
+ private static void ProcessarGNGGA(string sentenca)
+ {
+ string[] campos = sentenca.Split(',');
+
+ if (campos.Length < 10)
+ throw new FormatException("GNGGA incompleta.");
+
+ string horaUtc = GetField(campos, 1);
+ string latRaw = GetField(campos, 2);
+ string latHem = GetField(campos, 3);
+ string lonRaw = GetField(campos, 4);
+ string lonHem = GetField(campos, 5);
+ string qualidadeRaw = GetField(campos, 6, "0");
+ string satRaw = GetField(campos, 7, "0");
+ string hdopRaw = GetField(campos, 8, "99.9");
+ string altRaw = GetField(campos, 9, "0");
+ string geoidRaw = GetField(campos, 11, "0");
+ string idadeRaw = GetField(campos, 13);
+ string baseId = StripChecksum(GetField(campos, 14));
+
+ double latitude = 0;
+ double longitude = 0;
+ bool possuiLat = TryParseDmm(
+ latRaw,
+ latHem,
+ 2,
+ out latitude
+ );
+ bool possuiLon = TryParseDmm(
+ lonRaw,
+ lonHem,
+ 3,
+ out longitude
+ );
+
+ double altMsl;
+ double geoid;
+ double hdop;
+ int sats;
+ int fixCode;
+
+ double.TryParse(
+ altRaw,
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out altMsl
+ );
+
+ double.TryParse(
+ geoidRaw,
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out geoid
+ );
+
+ double.TryParse(
+ hdopRaw,
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out hdop
+ );
+
+ int.TryParse(satRaw, out sats);
+ int.TryParse(qualidadeRaw, out fixCode);
+
+ double idadeCorrecao = -1;
+ double idadeValida;
+
+ if (!string.IsNullOrWhiteSpace(idadeRaw) &&
+ double.TryParse(
+ idadeRaw,
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out idadeValida))
+ {
+ idadeCorrecao = idadeValida;
+ }
+
+ CopiarPosicaoParaPenultima();
+
+ UltimaLeitura.TimestampPos.valor =
+ Stopwatch.GetTimestamp() /
+ (double)Stopwatch.Frequency;
+
+ UltimaLeitura.Momento = DateTime.Now;
+
+ if (possuiLat && possuiLon)
+ {
+ UltimaLeitura.LatitudeAnt = latitude;
+ UltimaLeitura.LongitudeAnt = longitude;
+ }
+
+ UltimaLeitura.Altitude = altMsl;
+ UltimaLeitura.AltitudeElipsoidal = altMsl + geoid;
+ UltimaLeitura.PrecisaoHorizontal = hdop;
+ UltimaLeitura.NumeroSatelites = sats;
+ UltimaLeitura.QualidadeFix = (TiposCorrecaoGPS)fixCode;
+ UltimaLeitura.IdadeCorrecao = idadeCorrecao;
+ UltimaLeitura.BaseID = baseId;
+
+ if (possuiLat && possuiLon)
+ AplicarPosicaoCorrigida();
+
+ DateTime dataHora;
+
+ if (TryParseNmeaTime(horaUtc, out dataHora))
+ UltimaLeitura.DataHora = dataHora;
+
+ if (!UltimaLeitura.EnuOriginSet &&
+ UltimaLeitura.QualidadeFix ==
+ TiposCorrecaoGPS.RTKFixo &&
+ possuiLat &&
+ possuiLon)
+ {
+ UltimaLeitura.Lat0 = latitude;
+ UltimaLeitura.Lon0 = longitude;
+ UltimaLeitura.EnuOriginSet = true;
+ }
+ }
+
+ private static void CopiarPosicaoParaPenultima()
+ {
+ PenultimaLeitura.TimestampPos =
+ UltimaLeitura.TimestampPos.Clone();
+
+ PenultimaLeitura.Momento = UltimaLeitura.Momento;
+ PenultimaLeitura.Latitude = UltimaLeitura.Latitude;
+ PenultimaLeitura.Longitude = UltimaLeitura.Longitude;
+ PenultimaLeitura.LatitudeAnt = UltimaLeitura.LatitudeAnt;
+ PenultimaLeitura.LongitudeAnt = UltimaLeitura.LongitudeAnt;
+ PenultimaLeitura.Altitude = UltimaLeitura.Altitude;
+ PenultimaLeitura.AltitudeElipsoidal =
+ UltimaLeitura.AltitudeElipsoidal;
+
+ PenultimaLeitura.PrecisaoHorizontal =
+ UltimaLeitura.PrecisaoHorizontal;
+
+ PenultimaLeitura.NumeroSatelites =
+ UltimaLeitura.NumeroSatelites;
+
+ PenultimaLeitura.QualidadeFix =
+ UltimaLeitura.QualidadeFix;
+
+ PenultimaLeitura.DataHora = UltimaLeitura.DataHora;
+ PenultimaLeitura.IdadeCorrecao =
+ UltimaLeitura.IdadeCorrecao;
+
+ PenultimaLeitura.BaseID = UltimaLeitura.BaseID;
+ }
+
+ private static void ProcessarGNVTG(string sentenca)
+ {
+ string[] campos = sentenca.Split(',');
+
+ if (campos.Length < 8)
+ throw new FormatException("VTG incompleta.");
+
+ double curso;
+ double velocidadeKmh;
+
+ double.TryParse(
+ campos[1],
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out curso
+ );
+
+ double.TryParse(
+ campos[7],
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out velocidadeKmh
+ );
+
+ PenultimaLeitura.Momento = UltimaLeitura.Momento;
+ PenultimaLeitura.CursoVerdadeiro =
+ UltimaLeitura.CursoVerdadeiro;
+
+ PenultimaLeitura.Velocidade =
+ UltimaLeitura.Velocidade;
+
+ UltimaLeitura.Momento = DateTime.Now;
+ UltimaLeitura.CursoVerdadeiro = curso;
+ UltimaLeitura.Velocidade = velocidadeKmh;
+ }
+
+ private static void ProcessarGSV(string sentenca)
+ {
+ string[] campos = sentenca.Split(',');
+
+ if (campos.Length < 4 || sentenca.Length < 3)
+ throw new FormatException("GSV incompleta.");
+
+ string tipoSistema = sentenca.Substring(1, 2);
+
+ int sentAtual;
+ int sentTotal;
+ int visiveis;
+
+ int.TryParse(campos[2], out sentAtual);
+ int.TryParse(campos[1], out sentTotal);
+ int.TryParse(campos[3], out visiveis);
+
+ if (UltimaLeitura.SatelitesEmVista == null)
+ {
+ UltimaLeitura.SatelitesEmVista =
+ new List();
+ }
+
+ PenultimaLeitura.Momento = UltimaLeitura.Momento;
+ PenultimaLeitura.SatelitesEmVista =
+ new List(
+ UltimaLeitura.SatelitesEmVista
+ );
+
+ UltimaLeitura.Momento = DateTime.Now;
+
+ GPSSatelitesEmVistaModel leitura =
+ UltimaLeitura.SatelitesEmVista
+ .FirstOrDefault(x => x.TipoSistema == tipoSistema);
+
+ if (leitura == null)
+ {
+ leitura = new GPSSatelitesEmVistaModel
{
- if (latHem == "S") lat = -lat;
- if (lonHem == "W") lon = -lon;
+ TipoSistema = tipoSistema,
+ Sentencas =
+ new List()
+ };
- // mantém histórico
- PenultimaLeitura.Latitude = UltimaLeitura.Latitude;
- PenultimaLeitura.Longitude = UltimaLeitura.Longitude;
- PenultimaLeitura.LatitudeAnt = UltimaLeitura.LatitudeAnt;
- PenultimaLeitura.LongitudeAnt = UltimaLeitura.LongitudeAnt;
-
- UltimaLeitura.LatitudeAnt = lat;
- UltimaLeitura.LongitudeAnt = lon;
-
- AplicarPosicaoCorrigida();
- }
+ UltimaLeitura.SatelitesEmVista.Add(leitura);
}
- // atualiza hora se veio no frame
- if (!string.IsNullOrEmpty(horaUTC) && horaUTC.Length >= 6)
+ GPSSatelitesEmVistaSentencaModel parte =
+ leitura.Sentencas.FirstOrDefault(
+ x => x.SentencaAtual == sentAtual
+ );
+
+ if (parte == null)
{
- // hhmmss(.ss)
- var hh = horaUTC.Substring(0, 2);
- var mm = horaUTC.Substring(2, 2);
- var ss = horaUTC.Substring(4);
- if (TimeSpan.TryParseExact($"{hh}:{mm}:{ss}", @"hh\:mm\:ss\.ff",
- CultureInfo.InvariantCulture, out var tod)
- || TimeSpan.TryParseExact($"{hh}:{mm}:{ss}", @"hh\:mm\:ss",
- CultureInfo.InvariantCulture, out tod))
+ parte = new GPSSatelitesEmVistaSentencaModel
{
- UltimaLeitura.DataHora = DateTime.UtcNow.Date.Add(tod).ToLocalTime();
- }
+ SentencaAtual = sentAtual,
+ SentencasTotal = sentTotal,
+ QuantidadeSatelites = visiveis,
+ Dados =
+ new List()
+ };
+
+ leitura.Sentencas.Add(parte);
}
- // mapeia “mode” (se existir) para sua enum, sem sobrepor GGA melhor
- // Ex.: A=Autonomous(1), D=DGPS(2), R=RTK Fix(4), F=RTK Float(5)
- if (!string.IsNullOrEmpty(mode))
+ parte.Dados =
+ new List();
+
+ for (int i = 4; i + 3 < campos.Length; i += 4)
{
- if (mode == "R") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.RTKFixo;
- else if (mode == "F") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.RTKFlutuante;
- else if (mode == "D") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.DGPS;
- else if (mode == "E") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.DeadReckoing;
- else if (mode == "A") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.Autonomo;
- else if (mode == "N") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.SemCorrecao;
- else UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.SemCorrecao;
+ double elevacao;
+ double azimute;
+ double snr;
+
+ double.TryParse(
+ campos[i + 1],
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out elevacao
+ );
+
+ double.TryParse(
+ campos[i + 2],
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out azimute
+ );
+
+ double.TryParse(
+ StripChecksum(campos[i + 3]),
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out snr
+ );
+
+ parte.Dados.Add(
+ new GPSSatelitesEmVistaDadosModel
+ {
+ PRN = campos[i],
+ Elevacao = elevacao,
+ Azimute = azimute,
+ QualidadeSinal = snr
+ }
+ );
+ }
+ }
+
+ private static void ProcessarGNTHS(string sentenca)
+ {
+ string conteudo = sentenca.Trim();
+
+ if (conteudo.StartsWith("$"))
+ conteudo = conteudo.Substring(1);
+
+ string[] partesChecksum = conteudo.Split(
+ new[] { '*' },
+ 2
+ );
+
+ string[] campos = partesChecksum[0].Split(',');
+
+ if (campos.Length < 3)
+ throw new FormatException("THS incompleta.");
+
+ string headingTexto =
+ (campos[1] ?? string.Empty).Trim();
+
+ string status =
+ (campos[2] ?? string.Empty)
+ .Trim()
+ .ToUpperInvariant();
+
+ double headingTrue;
+
+ bool numerico = double.TryParse(
+ headingTexto,
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out headingTrue
+ );
+
+ bool valido =
+ status == "A" &&
+ numerico &&
+ !double.IsNaN(headingTrue) &&
+ !double.IsInfinity(headingTrue);
+
+ PenultimaLeitura.TimestampOri.valor =
+ UltimaLeitura.TimestampOri.valor;
+
+ PenultimaLeitura.Momento =
+ UltimaLeitura.Momento;
+
+ PenultimaLeitura.OrientacaoReal =
+ UltimaLeitura.OrientacaoReal;
+
+ PenultimaLeitura.TipoOrientacao =
+ UltimaLeitura.TipoOrientacao;
+
+ UltimaLeitura.TimestampOri.valor =
+ Stopwatch.GetTimestamp() /
+ (double)Stopwatch.Frequency;
+
+ UltimaLeitura.Momento = DateTime.Now;
+
+ if (!valido)
+ {
+ UltimaLeitura.OrientacaoReal = 9999;
+ UltimaLeitura.TipoOrientacao = "V";
+ return;
}
- AtualizarCoordenadasGPS();
+ headingTrue = GPSUtils.NormalizarAngulo(headingTrue);
+
+ UltimaLeitura.OrientacaoReal =
+ InverterHeading
+ ? GPSUtils.NormalizarAngulo(
+ headingTrue - 180.0
+ )
+ : headingTrue;
+
+ UltimaLeitura.TipoOrientacao = "A";
+
+ AplicarPosicaoCorrigida();
+ DefinirAnguloCarro();
}
private static void ProcessarGxGSA(string sentenca)
{
- // aceita $GPGSA, $GNGSA, $GLGSA, $GAGSA, $BDGSA…
- var campos = sentenca.Split(',');
- if (campos.Length < 17) return;
+ string[] campos = sentenca.Split(',');
+
+ if (campos.Length < 17)
+ return;
+
+ TiposDimensaoCorrecaoGPS modoSolucao =
+ (TiposDimensaoCorrecaoGPS)
+ GPSUtils.ParseInt(campos[2]);
- string modoSelecao = campos[1]; // M/A
- TiposDimensaoCorrecaoGPS modoSolucao =(TiposDimensaoCorrecaoGPS)GPSUtils.ParseInt(campos[2]); // 1/2/3
- // satélites usados: campos[3]..campos[14]
int satsUsados = 0;
+
for (int i = 3; i <= 14 && i < campos.Length; i++)
- if (!string.IsNullOrWhiteSpace(campos[i])) satsUsados++;
+ {
+ if (!string.IsNullOrWhiteSpace(campos[i]))
+ satsUsados++;
+ }
double pdop = GPSUtils.ParseDouble(campos[15]);
double hdop = GPSUtils.ParseDouble(campos[16]);
- double vdop = (campos.Length > 17) ? GPSUtils.ParseDouble(campos[17].Split('*')[0]) : double.NaN;
- // Atualiza apenas o que faz sentido complementar
- if (!double.IsInfinity(hdop) && !double.IsNaN(hdop)) UltimaLeitura.PrecisaoHorizontal = hdop;
- if (satsUsados > 0) UltimaLeitura.NumeroSatelites = Math.Max(UltimaLeitura.NumeroSatelites, satsUsados);
+ double vdop = campos.Length > 17
+ ? GPSUtils.ParseDouble(
+ StripChecksum(campos[17])
+ )
+ : double.NaN;
- // Se você quiser guardar os DOPs:
- UltimaLeitura.PDOP = !double.IsInfinity(pdop) && !double.IsNaN(pdop) ? pdop : UltimaLeitura.PDOP;
- UltimaLeitura.VDOP = !double.IsInfinity(vdop) && !double.IsNaN(vdop) ? vdop : UltimaLeitura.VDOP;
+ if (!double.IsInfinity(hdop) &&
+ !double.IsNaN(hdop))
+ {
+ UltimaLeitura.PrecisaoHorizontal = hdop;
+ }
- // Mapeia modoSolucao (não é igual ao “fix quality” do GGA!)
- // 1=NoFix, 2=Fix2D, 3=Fix3D — pode guardar num campo próprio se tiver
- UltimaLeitura.FixDimensao = modoSolucao; // crie int FixDimensao na sua struct, se não existir
+ if (satsUsados > 0)
+ {
+ UltimaLeitura.NumeroSatelites =
+ Math.Max(
+ UltimaLeitura.NumeroSatelites,
+ satsUsados
+ );
+ }
+
+ if (!double.IsInfinity(pdop) &&
+ !double.IsNaN(pdop))
+ {
+ UltimaLeitura.PDOP = pdop;
+ }
+
+ if (!double.IsInfinity(vdop) &&
+ !double.IsNaN(vdop))
+ {
+ UltimaLeitura.VDOP = vdop;
+ }
+
+ UltimaLeitura.FixDimensao = modoSolucao;
}
private static void ProcessarGxRMC(string sentenca)
{
- // Aceita $GPRMC, $GNRMC, $GLRMC, $GARMC, $BDRMC...
- var raw = sentenca;
- var campos = raw.Split(',');
+ string[] campos = sentenca.Split(',');
- if (campos.Length < 12) return;
+ if (campos.Length < 10)
+ throw new FormatException("RMC incompleta.");
- string timeUTC = campos[1]; // hhmmss.ss
- string status = campos[2]; // A=ativo, V=inválido
- string latRaw = campos[3];
- string latHem = campos[4];
- string lonRaw = campos[5];
- string lonHem = campos[6];
- string spdKtsS = campos[7]; // knots
- string cogS = campos[8]; // course over ground (graus)
- string date = campos[9]; // ddmmyy
- string magVarS = campos[10]; // pode estar vazio
- string magHem = campos.Length > 11 ? campos[11] : "";
- // mode pode vir no campo 12 (sem checksum) ou 12 com outro e 13 com checksum, depende do firmware
- string mode = "";
- if (campos.Length > 12)
+ string timeUtc = GetField(campos, 1);
+ string status = GetField(campos, 2);
+ string latRaw = GetField(campos, 3);
+ string latHem = GetField(campos, 4);
+ string lonRaw = GetField(campos, 5);
+ string lonHem = GetField(campos, 6);
+ string spdKnotsRaw = GetField(campos, 7);
+ string cogRaw = GetField(campos, 8);
+ string dateRaw = GetField(campos, 9);
+ string magRaw = GetField(campos, 10);
+ string magHem = StripChecksum(GetField(campos, 11));
+
+ string mode = campos.Length > 12
+ ? StripChecksum(campos[12]).Trim()
+ : string.Empty;
+
+ bool valido =
+ string.Equals(
+ status,
+ "A",
+ StringComparison.OrdinalIgnoreCase
+ );
+
+ double lat;
+ double lon;
+
+ if (valido &&
+ TryParseDmm(latRaw, latHem, 2, out lat) &&
+ TryParseDmm(lonRaw, lonHem, 3, out lon))
{
- var tmp = campos[12];
- // tira checksum se estiver grudado
- int asterix = tmp.IndexOf('*');
- mode = (asterix >= 0 ? tmp.Substring(0, asterix) : tmp).Trim();
- // alguns mandam mais um campo (navStatus) e o checksum só no final
- if (mode.Length == 0 && campos.Length > 13)
- {
- tmp = campos[13];
- asterix = tmp.IndexOf('*');
- mode = (asterix >= 0 ? tmp.Substring(0, asterix) : tmp).Trim();
- }
+ PenultimaLeitura.Latitude =
+ UltimaLeitura.Latitude;
+
+ PenultimaLeitura.Longitude =
+ UltimaLeitura.Longitude;
+
+ PenultimaLeitura.LatitudeAnt =
+ UltimaLeitura.LatitudeAnt;
+
+ PenultimaLeitura.LongitudeAnt =
+ UltimaLeitura.LongitudeAnt;
+
+ UltimaLeitura.LatitudeAnt = lat;
+ UltimaLeitura.LongitudeAnt = lon;
+
+ AplicarPosicaoCorrigida();
}
- bool valido = status == "A";
+ double spdKnots;
- // Converte lat/lon se válidos
- if (valido && !string.IsNullOrWhiteSpace(latRaw) && !string.IsNullOrWhiteSpace(lonRaw))
+ if (double.TryParse(
+ spdKnotsRaw,
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out spdKnots))
{
- if (latRaw.Length >= 4 && lonRaw.Length >= 5)
- {
- double lat = GPSUtils.DmmToDecimal(latRaw, 2);
- double lon = GPSUtils.DmmToDecimal(lonRaw, 3);
- if (!double.IsNaN(lat) && !double.IsInfinity(lat) && !double.IsNaN(lon) && !double.IsInfinity(lon))
- {
- if (latHem == "S") lat = -lat;
- if (lonHem == "W") lon = -lon;
-
- PenultimaLeitura.Latitude = UltimaLeitura.Latitude;
- PenultimaLeitura.Longitude = UltimaLeitura.Longitude;
- PenultimaLeitura.LatitudeAnt = UltimaLeitura.LatitudeAnt;
- PenultimaLeitura.LongitudeAnt = UltimaLeitura.LongitudeAnt;
-
- UltimaLeitura.LatitudeAnt = lat;
- UltimaLeitura.LongitudeAnt = lon;
-
- AplicarPosicaoCorrigida();
- }
- }
+ /*
+ * Mantém a semântica histórica do projeto:
+ * o campo recebe o valor da sentença em knots.
+ */
+ UltimaLeitura.Velocidade = spdKnots;
}
- // Velocidade (knots -> m/s e km/h, se quiser guardar)
- if (double.TryParse(spdKtsS, NumberStyles.Float, CultureInfo.InvariantCulture, out double spdKts))
- {
- UltimaLeitura.Velocidade = spdKts;
- }
+ double cog;
- // Course over ground (graus)
- if (double.TryParse(cogS, NumberStyles.Float, CultureInfo.InvariantCulture, out double cog))
+ if (double.TryParse(
+ cogRaw,
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out cog))
+ {
UltimaLeitura.CursoVerdadeiro = cog;
+ }
- // Data/hora (UTC)
- // timeUTC: hhmmss(.ss), date: ddmmyy
- DateTime? dt = null;
- if (!string.IsNullOrEmpty(timeUTC) && timeUTC.Length >= 6 && !string.IsNullOrEmpty(date) && date.Length == 6)
+ DateTime dataHora;
+
+ if (TryParseRmcDateTime(
+ dateRaw,
+ timeUtc,
+ out dataHora))
{
- string hh = timeUTC.Substring(0, 2);
- string mm = timeUTC.Substring(2, 2);
- string ss = timeUTC.Substring(4, 2);
+ UltimaLeitura.DataHora =
+ dataHora.ToLocalTime();
+ }
- string dd = date.Substring(0, 2);
- string MM = date.Substring(2, 2);
- string yy = date.Substring(4, 2);
+ double mag;
- // yy -> 20yy (assumindo 2000+; ajuste se precisar 19xx)
- int year = 2000 + int.Parse(yy, CultureInfo.InvariantCulture);
- if (int.TryParse(dd, out int d) && int.TryParse(MM, out int M) &&
- int.TryParse(hh, out int H) && int.TryParse(mm, out int m) && int.TryParse(ss, out int s))
+ if (double.TryParse(
+ magRaw,
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out mag))
+ {
+ if (string.Equals(
+ magHem,
+ "W",
+ StringComparison.OrdinalIgnoreCase))
{
- try
- {
- dt = new DateTime(year, M, d, H, m, s, DateTimeKind.Utc);
- }
- catch { /* ignora datas inválidas */ }
+ mag = -mag;
}
- }
- if (dt.HasValue) UltimaLeitura.DataHora = dt.Value.ToLocalTime();
- // Variação magnética (se quiser armazenar)
- if (double.TryParse(magVarS, NumberStyles.Float, CultureInfo.InvariantCulture, out double magVar))
- {
- if (magHem == "W") magVar = -magVar;
- UltimaLeitura.VariacaoMagnetica = magVar;
- }
-
- // Mode → promove QualidadeFix (não rebaixa)
- // A=Autônomo, D=DGPS, R=RTK Fix, F=RTK Float, N=No Fix
- if (!string.IsNullOrEmpty(mode))
- {
- if (mode == "R") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.RTKFixo;
- else if (mode == "F") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.RTKFlutuante;
- else if (mode == "D") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.DGPS;
- else if (mode == "E") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.DeadReckoing;
- else if (mode == "A") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.Autonomo;
- else if (mode == "N") UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.SemCorrecao;
- else UltimaLeitura.QualidadeFix = TiposCorrecaoGPS.SemCorrecao;
+ UltimaLeitura.VariacaoMagnetica = mag;
}
+ AplicarModoRmc(mode);
AtualizarCoordenadasGPS();
}
-
- private static async Task AplicarCorrecaoRTK_Ntrip()
+ private static void AplicarModoRmc(string mode)
{
- if (!Iniciado || LoopRTK_Ntrip || !APIService.HasInternet)
+ if (string.IsNullOrWhiteSpace(mode))
return;
- LoopRTK_Ntrip = true;
+ switch (mode.Trim().ToUpperInvariant())
+ {
+ case "R":
+ UltimaLeitura.QualidadeFix =
+ TiposCorrecaoGPS.RTKFixo;
+ break;
- // Configurações do NTRIP caster para RTK2Go
- string host = "gps-ntrip.ibge.gov.br";
- int port = 2101;
- string mountpoint = "EESC0";
- string username = "Zendion"; // Geralmente vazio para RTK2Go
- string password = "QD&m1p60"; // Geralmente vazio para RTK2Go
+ case "F":
+ UltimaLeitura.QualidadeFix =
+ TiposCorrecaoGPS.RTKFlutuante;
+ break;
- while (CorrecaoRTK_Ntrip && APIService.HasInternet) // Loop para reconectar em caso de falha
+ case "D":
+ UltimaLeitura.QualidadeFix =
+ TiposCorrecaoGPS.DGPS;
+ break;
+
+ case "E":
+ UltimaLeitura.QualidadeFix =
+ TiposCorrecaoGPS.DeadReckoing;
+ break;
+
+ case "A":
+ UltimaLeitura.QualidadeFix =
+ TiposCorrecaoGPS.Autonomo;
+ break;
+
+ case "N":
+ UltimaLeitura.QualidadeFix =
+ TiposCorrecaoGPS.SemCorrecao;
+ break;
+ }
+ }
+
+ // ============================================================
+ // RTCM VIA MQTT / FILA DE ESCRITA
+ // ============================================================
+
+ public static void AplicarCorrecaoRTK_Mqtt(
+ byte[] correcao,
+ int bytesRead)
+ {
+ if (correcao == null ||
+ bytesRead <= 0 ||
+ bytesRead > correcao.Length ||
+ bytesRead > RtcmMaxPayloadBytes)
+ {
+ Interlocked.Increment(ref _rtcmDroppedInvalid);
+ return;
+ }
+
+ byte[] copia = new byte[bytesRead];
+ Buffer.BlockCopy(correcao, 0, copia, 0, bytesRead);
+
+ EnfileirarRtcm(copia, RtcmSource.Mqtt);
+ }
+
+ private static void EnfileirarRtcm(
+ byte[] bytes,
+ RtcmSource source)
+ {
+ EnsureBackgroundWorkersStarted();
+
+ long agora = Stopwatch.GetTimestamp();
+
+ Interlocked.Increment(ref _rtcmReceived);
+ Interlocked.Exchange(ref _lastRtcmReceivedMono, agora);
+
+ var envelope = new RtcmEnvelope
+ {
+ Bytes = bytes,
+ ReceivedMonotonic = agora,
+ ReceivedUtc = DateTime.UtcNow,
+ Sequence = Interlocked.Increment(
+ ref _rtcmSequence
+ ),
+ Source = source,
+ MessageType = TryGetRtcmMessageType(bytes)
+ };
+
+ lock (_rtcmQueueLock)
+ {
+ while (_rtcmQueue.Count >= RtcmQueueCapacity)
+ {
+ _rtcmQueue.Dequeue();
+ Interlocked.Increment(
+ ref _rtcmDroppedQueue
+ );
+ }
+
+ _rtcmQueue.Enqueue(envelope);
+ Interlocked.Increment(ref _rtcmQueued);
+ }
+
+ try { _rtcmSignal.Release(); }
+ catch (SemaphoreFullException) { }
+ }
+
+ private static async Task RtcmWriterLoopAsync(
+ CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
{
try
{
- // Construa o cabeçalho da solicitação
- string credentials = string.IsNullOrEmpty(username)
- ? ""
- : Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
+ await _rtcmSignal.WaitAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
- string request = $"GET /{mountpoint} HTTP/1.0\r\n" +
- $"User-Agent: NTRIP Client/2.0\r\n" +
- $"Accept: */*\r\n" +
- $"Connection: keep-alive\r\n" +
- (!string.IsNullOrEmpty(credentials) ? $"Authorization: Basic {credentials}\r\n" : "") +
- "\r\n";
+ while (true)
+ {
+ RtcmEnvelope envelope = null;
- // Estabeleça a conexão
- using (TcpClient client = new TcpClient(host, port))
- using (NetworkStream stream = client.GetStream())
- using (StreamWriter writer = new StreamWriter(stream, Encoding.ASCII))
+ lock (_rtcmQueueLock)
{
- writer.Write(request);
- writer.Flush();
+ if (_rtcmQueue.Count > 0)
+ envelope = _rtcmQueue.Dequeue();
+ }
- // Leia a resposta
- using (StreamReader reader = new StreamReader(stream, Encoding.ASCII))
+ if (envelope == null)
+ break;
+
+ double idadeMs = GetAgeMs(
+ envelope.ReceivedMonotonic
+ );
+
+ if (idadeMs > RtcmMaxAgeMs)
+ {
+ Interlocked.Increment(
+ ref _rtcmDroppedStale
+ );
+ continue;
+ }
+
+ if (!Iniciado)
+ {
+ Interlocked.Increment(
+ ref _rtcmDroppedQueue
+ );
+ continue;
+ }
+
+ try
+ {
+ await _serialWriteLock
+ .WaitAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ try
{
- string response = await reader.ReadLineAsync();
- if (CorrecaoRTK_Ntrip)
- {
- if ((response ?? "").Contains("200 OK"))
- {
- Variaveis.MostrarLog($"[GPSService.AplicarCorrecaoRTK_Ntrip] Conexão bem-sucedida ao mountpoint!");
+ /*
+ * Verifica novamente depois de esperar o lock.
+ * A configuração pode ter ocupado a serial.
+ */
+ idadeMs = GetAgeMs(
+ envelope.ReceivedMonotonic
+ );
- byte[] buffer = new byte[4096];
- int bytesRead;
- while (CorrecaoRTK_Ntrip && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
- {
- try
- {
- if (PortaGPS?.IsOpen ?? false)
- {
- PortaGPS.Write(buffer, 0, bytesRead); // Envia os dados RTCM para o GPS
- //Console.WriteLine($"Enviando {bytesRead} bytes de correção RTCM para o GPS");
- UltimoEnvioCorrecaoRTK = DateTime.Now;
- }
- }
- catch (Exception ex)
- {
- Variaveis.MostrarLog($"[GPSService.AplicarCorrecaoRTK_Ntrip] Erro ao enviar correção RTCM para o módulo GNSS: {ex.Message}");
- }
- }
- }
- else
- {
- Variaveis.MostrarLog($"[GPSService.AplicarCorrecaoRTK_Ntrip] Falha na conexão com NTRIP: {response}");
- await Task.Delay(5000); // Aguarde antes de tentar novamente
- break;
- }
+ if (idadeMs > RtcmMaxAgeMs)
+ {
+ Interlocked.Increment(
+ ref _rtcmDroppedStale
+ );
+ continue;
}
+
+ WriteSerialUnsafe(
+ envelope.Bytes,
+ envelope.Bytes.Length
+ );
+
+ UltimoEnvioCorrecaoRTK =
+ DateTime.Now;
+
+ Interlocked.Exchange(
+ ref _lastRtcmWrittenMono,
+ Stopwatch.GetTimestamp()
+ );
+
+ Interlocked.Increment(
+ ref _rtcmWritten
+ );
+ }
+ finally
+ {
+ _serialWriteLock.Release();
}
}
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ catch (Exception ex)
+ {
+ Interlocked.Increment(
+ ref _rtcmWriteErrors
+ );
+
+ RecordError(
+ "[GPSService.RtcmWriter] " +
+ ex.Message
+ );
+ }
}
+ }
+ }
+
+ private static int TryGetRtcmMessageType(byte[] bytes)
+ {
+ /*
+ * RTCM3:
+ * D3 | 6 bits reservados + 10 bits length | payload
+ * Os 12 primeiros bits do payload formam o message type.
+ */
+ if (bytes == null ||
+ bytes.Length < 5 ||
+ bytes[0] != 0xD3)
+ {
+ return -1;
+ }
+
+ return ((bytes[3] << 4) | (bytes[4] >> 4))
+ & 0x0FFF;
+ }
+
+ // ============================================================
+ // NTRIP
+ // ============================================================
+
+ public static async Task IniciarNtripAsync()
+ {
+ if (Variaveis.IsAgroMonitor ||
+ !CorrecaoRTK_Ntrip)
+ {
+ return;
+ }
+
+ lock (_workerLock)
+ {
+ if (_ntripTask != null &&
+ !_ntripTask.IsCompleted)
+ {
+ return;
+ }
+
+ _ntripCts = new CancellationTokenSource();
+ _ntripTask = Task.Run(
+ () => AplicarCorrecaoRTK_Ntrip(
+ _ntripCts.Token
+ )
+ );
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task PararNtripAsync()
+ {
+ CancellationTokenSource cts;
+ Task task;
+ TcpClient client;
+
+ lock (_workerLock)
+ {
+ cts = _ntripCts;
+ task = _ntripTask;
+ client = _ntripClient;
+
+ _ntripCts = null;
+ _ntripTask = null;
+ _ntripClient = null;
+ }
+
+ CorrecaoRTK_Ntrip = false;
+
+ if (cts != null)
+ {
+ try { cts.Cancel(); } catch { }
+ }
+
+ if (client != null)
+ {
+ try { client.Close(); } catch { }
+ try { client.Dispose(); } catch { }
+ }
+
+ if (task != null)
+ {
+ try { await task.ConfigureAwait(false); }
+ catch (OperationCanceledException) { }
catch (Exception ex)
{
- Variaveis.MostrarLog($"[GPSService.AplicarCorrecaoRTK_Ntrip] Erro na conexão RTK: {ex.Message}");
- await Task.Delay(5000); // Aguarde antes de tentar novamente
+ RecordError(
+ "[GPSService.PararNtripAsync] " +
+ ex.Message
+ );
}
}
- LoopRTK_Ntrip = false;
+ if (cts != null)
+ cts.Dispose();
}
- public static void AplicarCorrecaoRTK_Mqtt(byte[] correcao, int bytesRead)
+ private static async Task AplicarCorrecaoRTK_Ntrip(
+ CancellationToken cancellationToken)
{
- if (PortaGPS?.IsOpen ?? false)
+ if (!Iniciado ||
+ LoopRTK_Ntrip ||
+ !APIService.HasInternet)
{
- PortaGPS.Write(correcao, 0, bytesRead);
- UltimoEnvioCorrecaoRTK = DateTime.Now;
+ return;
+ }
+
+ LoopRTK_Ntrip = true;
+
+ string host = Environment.GetEnvironmentVariable(
+ "AGRO_NTRIP_HOST"
+ ) ?? "gps-ntrip.ibge.gov.br";
+
+ int port = 2101;
+ int portEnv;
+
+ if (int.TryParse(
+ Environment.GetEnvironmentVariable(
+ "AGRO_NTRIP_PORT"
+ ),
+ out portEnv))
+ {
+ port = portEnv;
+ }
+
+ string mountpoint =
+ Environment.GetEnvironmentVariable(
+ "AGRO_NTRIP_MOUNTPOINT"
+ ) ?? "EESC0";
+
+ string username = "Zendion";
+ //Environment.GetEnvironmentVariable(
+ // "AGRO_NTRIP_USERNAME"
+ //) ?? string.Empty;
+
+ string password = "QD&m1p60";
+ //Environment.GetEnvironmentVariable(
+ // "AGRO_NTRIP_PASSWORD"
+ //) ?? string.Empty;
+
+ int backoffMs = 1000;
+ var random = new Random();
+
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested &&
+ CorrecaoRTK_Ntrip &&
+ APIService.HasInternet)
+ {
+ TcpClient client = null;
+
+ try
+ {
+ client = new TcpClient();
+
+ lock (_workerLock)
+ _ntripClient = client;
+
+ Task connectTask =
+ client.ConnectAsync(host, port);
+
+ Task completed = await Task.WhenAny(
+ connectTask,
+ Task.Delay(
+ 10000,
+ cancellationToken
+ )
+ ).ConfigureAwait(false);
+
+ if (completed != connectTask)
+ throw new TimeoutException(
+ "Timeout ao conectar ao NTRIP."
+ );
+
+ await connectTask.ConfigureAwait(false);
+
+ NetworkStream stream =
+ client.GetStream();
+
+ string credentials =
+ string.IsNullOrEmpty(username)
+ ? string.Empty
+ : Convert.ToBase64String(
+ Encoding.ASCII.GetBytes(
+ username + ":" + password
+ )
+ );
+
+ string request =
+ "GET /" + mountpoint +
+ " HTTP/1.0\r\n" +
+ "User-Agent: NTRIP AgroBase/2.0\r\n" +
+ "Accept: */*\r\n" +
+ "Connection: close\r\n" +
+ (
+ string.IsNullOrEmpty(credentials)
+ ? string.Empty
+ : "Authorization: Basic " +
+ credentials + "\r\n"
+ ) +
+ "\r\n";
+
+ byte[] requestBytes =
+ Encoding.ASCII.GetBytes(request);
+
+ await stream.WriteAsync(
+ requestBytes,
+ 0,
+ requestBytes.Length,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ NtripHeaderResult header =
+ await ReadNtripHeaderAsync(
+ stream,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (!header.Success)
+ {
+ throw new IOException(
+ "Resposta NTRIP inválida: " +
+ header.StatusLine
+ );
+ }
+
+ Variaveis.MostrarLog(
+ "[GPSService.NTRIP] Conectado a " +
+ host + "/" + mountpoint
+ );
+
+ backoffMs = 1000;
+
+ if (header.RemainingBytes != null &&
+ header.RemainingBytes.Length > 0)
+ {
+ Interlocked.Add(
+ ref _ntripBytesReceived,
+ header.RemainingBytes.Length
+ );
+
+ EnfileirarRtcm(
+ header.RemainingBytes,
+ RtcmSource.Ntrip
+ );
+ }
+
+ byte[] buffer = new byte[4096];
+
+ while (!cancellationToken
+ .IsCancellationRequested &&
+ CorrecaoRTK_Ntrip)
+ {
+ int bytesRead = await stream.ReadAsync(
+ buffer,
+ 0,
+ buffer.Length,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (bytesRead <= 0)
+ break;
+
+ byte[] bloco = new byte[bytesRead];
+
+ Buffer.BlockCopy(
+ buffer,
+ 0,
+ bloco,
+ 0,
+ bytesRead
+ );
+
+ Interlocked.Add(
+ ref _ntripBytesReceived,
+ bytesRead
+ );
+
+ EnfileirarRtcm(
+ bloco,
+ RtcmSource.Ntrip
+ );
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ Interlocked.Increment(
+ ref _ntripErrors
+ );
+
+ RecordError(
+ "[GPSService.NTRIP] " + ex.Message
+ );
+ }
+ finally
+ {
+ lock (_workerLock)
+ {
+ if (ReferenceEquals(
+ _ntripClient,
+ client))
+ {
+ _ntripClient = null;
+ }
+ }
+
+ if (client != null)
+ {
+ try { client.Close(); } catch { }
+ try { client.Dispose(); } catch { }
+ }
+ }
+
+ if (cancellationToken.IsCancellationRequested ||
+ !CorrecaoRTK_Ntrip)
+ {
+ break;
+ }
+
+ Interlocked.Increment(ref _ntripReconnects);
+
+ int jitter = random.Next(0, 350);
+
+ await Task.Delay(
+ backoffMs + jitter,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ backoffMs = Math.Min(
+ 30000,
+ (int)(backoffMs * 1.8)
+ );
+ }
+ }
+ finally
+ {
+ LoopRTK_Ntrip = false;
}
}
+ private static async Task
+ ReadNtripHeaderAsync(
+ NetworkStream stream,
+ CancellationToken cancellationToken)
+ {
+ var bytes = new List(2048);
+ byte[] buffer = new byte[512];
- private static bool PossuiHeadingValido(GPSModel leitura)
+ while (bytes.Count < 16 * 1024)
+ {
+ int read = await stream.ReadAsync(
+ buffer,
+ 0,
+ buffer.Length,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (read <= 0)
+ break;
+
+ for (int i = 0; i < read; i++)
+ bytes.Add(buffer[i]);
+
+ int headerEnd = FindHeaderEnd(bytes);
+
+ if (headerEnd >= 0)
+ {
+ byte[] all = bytes.ToArray();
+ string headerText = Encoding.ASCII.GetString(
+ all,
+ 0,
+ headerEnd
+ );
+
+ string statusLine = headerText
+ .Split(new[] { "\r\n" },
+ StringSplitOptions.None)
+ .FirstOrDefault() ?? string.Empty;
+
+ bool success =
+ statusLine.IndexOf(
+ "200",
+ StringComparison.OrdinalIgnoreCase
+ ) >= 0 ||
+ statusLine.StartsWith(
+ "ICY 200",
+ StringComparison.OrdinalIgnoreCase
+ );
+
+ int payloadStart = headerEnd + 4;
+ int remainingLength =
+ all.Length - payloadStart;
+
+ byte[] remaining =
+ remainingLength > 0
+ ? all.Skip(payloadStart)
+ .Take(remainingLength)
+ .ToArray()
+ : new byte[0];
+
+ return new NtripHeaderResult
+ {
+ Success = success,
+ StatusLine = statusLine,
+ RemainingBytes = remaining
+ };
+ }
+ }
+
+ return new NtripHeaderResult
+ {
+ Success = false,
+ StatusLine = "Cabeçalho NTRIP incompleto.",
+ RemainingBytes = new byte[0]
+ };
+ }
+
+ private static int FindHeaderEnd(List bytes)
+ {
+ for (int i = 3; i < bytes.Count; i++)
+ {
+ if (bytes[i - 3] == 13 &&
+ bytes[i - 2] == 10 &&
+ bytes[i - 1] == 13 &&
+ bytes[i] == 10)
+ {
+ return i - 3;
+ }
+ }
+
+ return -1;
+ }
+
+ // ============================================================
+ // SNAPSHOT E ESTADO GNSS
+ // ============================================================
+
+ public static GPSModel GetSnapshot()
+ {
+ lock (_stateLock)
+ return UltimaLeitura.Clone();
+ }
+
+ public static GPSModel GetPreviousSnapshot()
+ {
+ lock (_stateLock)
+ return PenultimaLeitura.Clone();
+ }
+
+ private static bool PossuiHeadingValido(
+ GPSModel leitura)
{
if (leitura == null)
return false;
@@ -1152,7 +2147,7 @@ namespace AgroBase.Services
return;
}
- (double latCor, double lonCorr) =
+ var corrigida =
LeverArm.FixLeverArmLatLon_Fast(
UltimaLeitura.LatitudeAnt,
UltimaLeitura.LongitudeAnt,
@@ -1160,213 +2155,449 @@ namespace AgroBase.Services
UltimaLeitura.TimestampOri.frequencia
);
- UltimaLeitura.Latitude = latCor;
- UltimaLeitura.Longitude = lonCorr;
+ UltimaLeitura.Latitude = corrigida.lat;
+ UltimaLeitura.Longitude = corrigida.lon;
}
public static void AtualizarCoordenadasGPS()
{
- PenultimaLeitura.Ntrip_ativado = UltimaLeitura.Ntrip_ativado;
- PenultimaLeitura.Heartbeat = UltimaLeitura.Heartbeat;
- PenultimaLeitura.LeverArmFrontal = UltimaLeitura.LeverArmFrontal;
- PenultimaLeitura.LeverArmLateral = UltimaLeitura.LeverArmLateral;
+ PenultimaLeitura.Ntrip_ativado =
+ UltimaLeitura.Ntrip_ativado;
- UltimaLeitura.Ntrip_ativado = CorrecaoRTK_Ntrip;
- UltimaLeitura.Heartbeat = (UltimaLeitura.Heartbeat + 1) % 10;
- UltimaLeitura.LeverArmFrontal = LeverArm?.FrontalTotalCm ?? 0;
- UltimaLeitura.LeverArmLateral = LeverArm?.LateralTotalCm ?? 0;
+ PenultimaLeitura.Heartbeat =
+ UltimaLeitura.Heartbeat;
+
+ PenultimaLeitura.LeverArmFrontal =
+ UltimaLeitura.LeverArmFrontal;
+
+ PenultimaLeitura.LeverArmLateral =
+ UltimaLeitura.LeverArmLateral;
+
+ UltimaLeitura.Ntrip_ativado =
+ CorrecaoRTK_Ntrip;
+
+ UltimaLeitura.Heartbeat =
+ (UltimaLeitura.Heartbeat + 1) % 10;
+
+ UltimaLeitura.LeverArmFrontal =
+ LeverArm != null
+ ? LeverArm.FrontalTotalCm
+ : 0;
+
+ UltimaLeitura.LeverArmLateral =
+ LeverArm != null
+ ? LeverArm.LateralTotalCm
+ : 0;
if (Variaveis.IsAgroMonitor)
- {
- //EnviarCoordenadasParaMapa(UltimaLeitura.Latitude, UltimaLeitura.Longitude, UltimaLeitura.AnguloCarroDefinido, false, LoRaBaseService.parametrosModel.address);
return;
+
+ UltimasLeituras.Add(
+ UltimaLeitura.Clone()
+ );
+
+ while (UltimasLeituras.Count >
+ Math.Max(1, TaxaAmostragemHz))
+ {
+ UltimasLeituras.RemoveAt(0);
}
- UltimasLeituras.Add(UltimaLeitura.Clone());
- if (UltimasLeituras.Count > TaxaAmostragemHz) UltimasLeituras.Remove(UltimasLeituras.First());
-
DefinirOrientacaoMovimento();
-
AtualizaDadosRedis();
if (Variaveis.OperacaoEmAndamento.Iniciado)
{
- var _Trajetoria = Variaveis.OperacaoEmAndamento.Trajetoria;
- CorredorTrajetoriaModel CorredorAtual = _Trajetoria?.CorredorAtual;
- var _GPSTrajetoria = Variaveis.OperacaoEmAndamento.GPSTrajetoria;
+ var trajetoria =
+ Variaveis.OperacaoEmAndamento.Trajetoria;
- // ✅ Criando uma cópia eficiente de UltimaLeitura sem copiar manualmente cada propriedade
- var novaLeitura = UltimaLeitura.Clone();
+ var gpsTrajetoria =
+ Variaveis.OperacaoEmAndamento
+ .GPSTrajetoria;
- // ✅ Adiciona a nova leitura à trajetória GPS
- _GPSTrajetoria.Add(novaLeitura);
+ GPSModel novaLeitura =
+ UltimaLeitura.Clone();
- // ✅ Calcula a distância entre os dois últimos pontos, mas só se houver pelo menos 2 pontos
- if (_GPSTrajetoria.Count > 1)
+ gpsTrajetoria.Add(novaLeitura);
+
+ if (gpsTrajetoria.Count > 1)
{
- int ultimo = _GPSTrajetoria.Count - 1;
- int penultimo = _GPSTrajetoria.Count - 2;
+ int ultimo =
+ gpsTrajetoria.Count - 1;
- double distancia = GPSUtils.DistanciaEntrePontos(
- _GPSTrajetoria[ultimo], // Último ponto
- _GPSTrajetoria[penultimo] // Penúltimo ponto
- );
+ int penultimo = ultimo - 1;
- _Trajetoria?.AtualizarDistanciaPercorrida(distancia);
+ double distancia =
+ GPSUtils.DistanciaEntrePontos(
+ gpsTrajetoria[ultimo],
+ gpsTrajetoria[penultimo]
+ );
+
+ if (trajetoria != null)
+ trajetoria.AtualizarDistanciaPercorrida(
+ distancia
+ );
}
- var bicos = Variaveis.OperacaoEmAndamento.DispAtu?.Dados?.BicosPulverizadores;
+ var bicos =
+ Variaveis.OperacaoEmAndamento
+ .DispAtu?
+ .Dados?
+ .BicosPulverizadores;
+
if (bicos != null)
{
foreach (var bico in bicos)
{
- if (!bico.Inicializado || !bico.Comandar || !bico.ComandoAtuar)
+ if (!bico.Inicializado ||
+ !bico.Comandar ||
+ !bico.ComandoAtuar)
+ {
continue;
+ }
- bico.AdicionarPontoTrechoAtivo(UltimaLeitura.Latitude, UltimaLeitura.Longitude);
+ bico.AdicionarPontoTrechoAtivo(
+ UltimaLeitura.Latitude,
+ UltimaLeitura.Longitude
+ );
}
}
}
- Variaveis.OperacaoEmAndamento.Trajetoria?.LoopAtualizaDados();
+ if (Variaveis.OperacaoEmAndamento.Trajetoria != null)
+ {
+ Variaveis.OperacaoEmAndamento
+ .Trajetoria
+ .LoopAtualizaDados();
+ }
AtualizarTrajetoriaDinamica();
- int EnderecoEquipamento = Convert.ToInt32(Variaveis.LoraService?.ParametrosGet?.address ?? Variaveis.LoraService?.ParametrosSet?.address ?? 0x00);
- EnviarCoordenadasParaMapa(UltimaLeitura.Latitude, UltimaLeitura.Longitude, UltimaLeitura.AnguloCarroDefinido, Variaveis.OperacaoEmAndamento.Iniciado, EnderecoEquipamento);
+ int enderecoEquipamento = Convert.ToInt32(
+ Variaveis.LoraService?
+ .ParametrosGet?
+ .address ??
+ Variaveis.LoraService?
+ .ParametrosSet?
+ .address ??
+ 0x00
+ );
- if (Variaveis.LoraService?.Iniciado ?? false && (Variaveis.LoraService?._ultimoRxDados ?? DateTime.MinValue) > DateTime.UtcNow.AddSeconds(-60))
+ EnviarCoordenadasParaMapa(
+ UltimaLeitura.Latitude,
+ UltimaLeitura.Longitude,
+ UltimaLeitura.AnguloCarroDefinido,
+ Variaveis.OperacaoEmAndamento.Iniciado,
+ enderecoEquipamento
+ );
+
+ if ((Variaveis.LoraService?.Iniciado ?? false) &&
+ (Variaveis.LoraService?._ultimoRxDados ??
+ DateTime.MinValue) >
+ DateTime.UtcNow.AddSeconds(-60))
{
- byte EnderecoBase = Variaveis.LoraBaseParametros.address;
- EnviarCoordenadasParaMapa(VariaveisOperacao.PosicaoBase.Latitude, VariaveisOperacao.PosicaoBase.Longitude, VariaveisOperacao.PosicaoBase.OrientacaoReal, false, EnderecoBase);
+ byte enderecoBase =
+ Variaveis.LoraBaseParametros.address;
+
+ GPSModel posicaoBase =
+ VariaveisOperacao
+ .ObterPosicaoBaseSnapshot();
+
+ EnviarCoordenadasParaMapa(
+ posicaoBase.Latitude,
+ posicaoBase.Longitude,
+ posicaoBase.OrientacaoReal,
+ false,
+ enderecoBase
+ );
}
- PenultimaLeitura.Inicializado = UltimaLeitura.Inicializado;
+ PenultimaLeitura.Inicializado =
+ UltimaLeitura.Inicializado;
+
UltimaLeitura.Inicializado = Iniciado;
- if (CorrecaoRTK_Ntrip && UltimoEnvioCorrecaoRTK.AddSeconds(TempoMin_Ntrip) < DateTime.Now)
+ if (CorrecaoRTK_Ntrip &&
+ GetAgeMs(
+ Interlocked.Read(
+ ref _lastRtcmWrittenMono
+ )
+ ) > TempoMin_Ntrip * 1000.0)
{
- UltimoEnvioCorrecaoRTK = DateTime.Now;
- LoopRTK_Ntrip = false;
- Task.Run(async () =>
- {
- await Task.Delay(5000);
- await AplicarCorrecaoRTK_Ntrip();
- });
+ _ = IniciarNtripAsync();
}
}
+ // ============================================================
+ // PUBLICAÇÃO LOCAL LATEST-ONLY
+ // ============================================================
-
- public static void EnviarCoordenadasParaMapa(double Latitude, double Longitude, double Orientacao, bool EmFoco, int ID)
+ public static void EnviarCoordenadasParaMapa(
+ double Latitude,
+ double Longitude,
+ double Orientacao,
+ bool EmFoco,
+ int ID)
{
- var Coordenadas = new
+ var coordenadas = new
{
latitude = Latitude,
- longitude = Longitude,
- orientacao = GPSUtils.NormalizarAngulo(Orientacao - 180.0),
- id = ID,
+ longitude = Longitude,
+ orientacao =
+ GPSUtils.NormalizarAngulo(
+ Orientacao - 180.0
+ ),
+ id = ID,
foco = EmFoco
};
- Task.Run(async () =>
- {
- //Console.WriteLine(JsonConvert.SerializeObject(Coordenadas));
- await Variaveis.MqttServiceLocal.PublishAsync(
- Variaveis.MqttServiceLocal.Topicos.First(x => x.Topico == MapasVariaveisModel.TopicoCoordenadasGPS),
- JsonConvert.SerializeObject(Coordenadas)
- );
- });
+
+ QueueLocalPublication(
+ MapasVariaveisModel.TopicoCoordenadasGPS,
+ JsonConvert.SerializeObject(coordenadas)
+ );
}
public static void AtualizarTrajetoriaDinamica()
{
- if (Variaveis.OperacaoEmAndamento.Trajetoria?._TrajetoriaDinamica?.Any() ?? false)
- {
- var Trajetoria = Variaveis.OperacaoEmAndamento.Trajetoria.TrajetoriaDinamica.Select(x => new
+ var dinamica =
+ Variaveis.OperacaoEmAndamento
+ .Trajetoria?
+ ._TrajetoriaDinamica;
+
+ if (!(dinamica?.Any() ?? false))
+ return;
+
+ var trajetoria = Variaveis
+ .OperacaoEmAndamento
+ .Trajetoria
+ .TrajetoriaDinamica
+ .Select(x => new
{
latitude = x.Latitude,
longitude = x.Longitude
- }).ToArray();
+ })
+ .ToArray();
- Task.Run(async () =>
+ QueueLocalPublication(
+ MapasVariaveisModel
+ .TopicoTrajetoriaDinamica,
+ JsonConvert.SerializeObject(trajetoria)
+ );
+ }
+
+ public static void AtualizarRuasSelecionadas(
+ List RuasSelecionadas)
+ {
+ /*
+ * Preserva o formato legado produzido anteriormente.
+ */
+ QueueLocalPublication(
+ MapasVariaveisModel
+ .TopicoSelecaoRuasMapa,
+ JsonConvert.SerializeObject(
+ "[" +
+ string.Join(
+ ",",
+ (RuasSelecionadas ??
+ new List()).ToArray()
+ ) +
+ "]"
+ )
+ );
+ }
+
+ private static void QueueLocalPublication(
+ string topic,
+ string payload)
+ {
+ if (string.IsNullOrWhiteSpace(topic))
+ return;
+
+ EnsureBackgroundWorkersStarted();
+
+ lock (_localPublishLock)
+ {
+ _localLatestPayload[topic] =
+ payload ?? string.Empty;
+ }
+
+ try { _localPublishSignal.Release(); }
+ catch (SemaphoreFullException) { }
+ }
+
+ private static async Task LocalPublisherLoopAsync(
+ CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
{
- await Variaveis.MqttServiceLocal.PublishAsync(
- Variaveis.MqttServiceLocal.Topicos.First(x => x.Topico == MapasVariaveisModel.TopicoTrajetoriaDinamica),
- JsonConvert.SerializeObject(Trajetoria)
+ await _localPublishSignal
+ .WaitAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+
+ Dictionary lote;
+
+ lock (_localPublishLock)
+ {
+ lote = new Dictionary(
+ _localLatestPayload,
+ StringComparer.Ordinal
);
- });
+
+ _localLatestPayload.Clear();
+ }
+
+ MqttService mqtt =
+ Variaveis.MqttServiceLocal;
+
+ if (mqtt == null ||
+ !mqtt.StatusConexao())
+ {
+ continue;
+ }
+
+ foreach (var item in lote)
+ {
+ if (cancellationToken
+ .IsCancellationRequested)
+ {
+ return;
+ }
+
+ MqttService.MqttTopicosModel topico =
+ mqtt.Topicos.FirstOrDefault(
+ x => x.Topico == item.Key
+ );
+
+ if (topico == null)
+ continue;
+
+ try
+ {
+ await mqtt.PublishAsync(
+ topico,
+ item.Value,
+ true
+ ).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ RecordError(
+ "[GPSService.LocalPublisher] " +
+ ex.Message
+ );
+ }
+ }
}
}
- public static void AtualizarRuasSelecionadas(List RuasSelecionadas)
- {
- Task.Run(async () =>
- {
- await Variaveis.MqttServiceLocal.PublishAsync(
- Variaveis.MqttServiceLocal.Topicos.First(x => x.Topico == MapasVariaveisModel.TopicoSelecaoRuasMapa),
- JsonConvert.SerializeObject("[" + string.Join(",", RuasSelecionadas.ToArray()) + "]"),
- true
- );
- });
- }
+ // ============================================================
+ // ORIENTAÇÃO, REDIS E HISTÓRICO
+ // ============================================================
private static void DefinirOrientacaoMovimento()
{
if (UltimasLeituras.Count < 2)
{
- double ang = GPSUtils.CalcularOrientacao(PenultimaLeitura, UltimaLeitura);
- double d = GPSUtils.DistanciaEntrePontos(PenultimaLeitura, UltimaLeitura);
+ double ang =
+ GPSUtils.CalcularOrientacao(
+ PenultimaLeitura,
+ UltimaLeitura
+ );
- PenultimaLeitura.OrientacaoMovimento = UltimaLeitura.OrientacaoMovimento;
- PenultimaLeitura.Distancia = UltimaLeitura.Distancia;
+ double d =
+ GPSUtils.DistanciaEntrePontos(
+ PenultimaLeitura,
+ UltimaLeitura
+ );
+
+ PenultimaLeitura.OrientacaoMovimento =
+ UltimaLeitura.OrientacaoMovimento;
+
+ PenultimaLeitura.Distancia =
+ UltimaLeitura.Distancia;
UltimaLeitura.OrientacaoMovimento = ang;
- UltimaLeitura.Distancia = d; // aqui fica sendo o deslocamento dessa “janela” mínima
+ UltimaLeitura.Distancia = d;
}
else
{
- // Média circular ponderada pela distância de cada segmento da janela
- double sumX = 0.0, sumY = 0.0;
- double distAcum = 0.0;
+ double sumX = 0;
+ double sumY = 0;
+ double distAcum = 0;
- for (int i = 0; i < UltimasLeituras.Count - 1; i++)
+ for (int i = 0;
+ i < UltimasLeituras.Count - 1;
+ i++)
{
- var a = UltimasLeituras[i];
- var b = UltimasLeituras[i + 1];
+ GPSModel a = UltimasLeituras[i];
+ GPSModel b = UltimasLeituras[i + 1];
- double angSeg = GPSUtils.CalcularOrientacao(a, b); // em graus
- double dSeg = GPSUtils.DistanciaEntrePontos(a, b); // em metros
- if (dSeg <= 0) continue; // ignora degrau zero
+ double angSeg =
+ GPSUtils.CalcularOrientacao(a, b);
- double rad = angSeg * Math.PI / 180.0;
- sumX += Math.Cos(rad) * dSeg; // peso = distância
+ double dSeg =
+ GPSUtils.DistanciaEntrePontos(a, b);
+
+ if (dSeg <= 0)
+ continue;
+
+ double rad =
+ angSeg * Math.PI / 180.0;
+
+ sumX += Math.Cos(rad) * dSeg;
sumY += Math.Sin(rad) * dSeg;
distAcum += dSeg;
}
- // Se tudo foi zero (parado), caia para heading instantâneo
double anguloMovimento;
+
if (distAcum <= 0)
{
- anguloMovimento = UltimaLeitura.OrientacaoReal; // heading como fallback parado
+ anguloMovimento =
+ UltimaLeitura.OrientacaoReal;
}
else
{
- anguloMovimento = Math.Atan2(sumY, sumX) * 180.0 / Math.PI;
- anguloMovimento = GPSUtils.NormalizarAngulo(anguloMovimento);
+ anguloMovimento =
+ Math.Atan2(sumY, sumX) *
+ 180.0 / Math.PI;
+
+ anguloMovimento =
+ GPSUtils.NormalizarAngulo(
+ anguloMovimento
+ );
}
- // Distância linear entre a 1ª e a última da janela (boa para "confiabilidade")
- var first = UltimasLeituras[0];
- var last = UltimasLeituras[UltimasLeituras.Count - 1];
- double distLinear = GPSUtils.DistanciaEntrePontos(first, last);
+ GPSModel first =
+ UltimasLeituras[0];
- // Atualiza campos
- PenultimaLeitura.OrientacaoMovimento = UltimaLeitura.OrientacaoMovimento;
- PenultimaLeitura.Distancia = UltimaLeitura.Distancia;
+ GPSModel last =
+ UltimasLeituras[
+ UltimasLeituras.Count - 1
+ ];
- UltimaLeitura.OrientacaoMovimento = anguloMovimento;
- UltimaLeitura.Distancia = distLinear; // use a linear como sinal de "Δpos confiável" para a fusão
+ double distLinear =
+ GPSUtils.DistanciaEntrePontos(
+ first,
+ last
+ );
+
+ PenultimaLeitura.OrientacaoMovimento =
+ UltimaLeitura.OrientacaoMovimento;
+
+ PenultimaLeitura.Distancia =
+ UltimaLeitura.Distancia;
+
+ UltimaLeitura.OrientacaoMovimento =
+ anguloMovimento;
+
+ UltimaLeitura.Distancia = distLinear;
}
DefinirAnguloCarro();
@@ -1374,178 +2605,253 @@ namespace AgroBase.Services
public static void DefinirAnguloCarro()
{
- double anguloFinal = 0.0;
+ double anguloFinal;
+
+ bool imuIniciado =
+ Variaveis.OperacaoEmAndamento
+ .Sensoriamento?
+ .IMU?
+ .Iniciado ?? false;
- bool imuIniciado = Variaveis.OperacaoEmAndamento.Sensoriamento?.IMU?.Iniciado ?? false;
bool gpsIniciado = Iniciado;
- bool headingValido = PossuiHeadingValido(UltimaLeitura);
- if (imuIniciado)
- {
- //Variaveis.OperacaoEmAndamento.DispSen.Dados?.DadosLeitura?.SensoresIMU?.FirstOrDefault()?.AtualizarOffset(UltimaLeitura);
- }
+ bool headingValido =
+ PossuiHeadingValido(UltimaLeitura);
if (Variaveis.OperacaoEmAndamento.Simulando)
{
- anguloFinal = UltimaLeitura.OrientacaoReal;
+ anguloFinal =
+ UltimaLeitura.OrientacaoReal;
}
else if (gpsIniciado && headingValido)
{
- anguloFinal = UltimaLeitura.OrientacaoReal;
+ anguloFinal =
+ UltimaLeitura.OrientacaoReal;
}
- else if (gpsIniciado && UltimaLeitura.Distancia > 0.25)
+ else if (gpsIniciado &&
+ UltimaLeitura.Distancia > 0.25)
{
- anguloFinal = UltimaLeitura.OrientacaoMovimento;
+ anguloFinal =
+ UltimaLeitura.OrientacaoMovimento;
}
else if (imuIniciado)
{
- anguloFinal = Variaveis.OperacaoEmAndamento.Sensoriamento.IMU.YawSeguro;
+ anguloFinal =
+ Variaveis.OperacaoEmAndamento
+ .Sensoriamento
+ .IMU
+ .YawSeguro;
}
else
{
- anguloFinal = UltimaLeitura.AnguloCarroDefinido;
+ anguloFinal =
+ UltimaLeitura.AnguloCarroDefinido;
}
- // Atualiza o ângulo final
- UltimaLeitura.AnguloCarroDefinido = anguloFinal; // GPSUtils.NormalizarAngulo(anguloFinal + HeadingOffsetSimulador + HeadingOffsetCorrecao);
+ UltimaLeitura.AnguloCarroDefinido =
+ anguloFinal;
}
public static double FundirHeadingComMovimento(
- double headingDeg, // OrientacaoReal (antena/corpo)
- double angMovDeg, // OrientacaoMovimento (calculada acima)
- double distLinearJanela, // UltimaLeitura.Distancia (Δpos linear entre 1ª e última amostra)
- double velPercent = 0.0, // se tiver velocidade filtrada, passe aqui; senão 0
- bool rtkFix = true, // se tiver essa info
- double angFundidoAnterior = double.NaN, // para low-pass; passe double.NaN para desabilitar
- double alfaLowPass = 0.25 // 0→sem low-pass; 0.2–0.35 costuma ser bom
- )
+ double headingDeg,
+ double angMovDeg,
+ double distLinearJanela,
+ double velPercent = 0.0,
+ bool rtkFix = true,
+ double angFundidoAnterior = double.NaN,
+ double alfaLowPass = 0.25)
{
- // Parâmetros sintonizados para 5 Hz
- double distMin = 0.03; // ~3 cm → considera "parado"
- double distFull = 0.25; // ~25 cm → deslocamento confiável
- double pesoMinHeading = 0.30; // mantém um "fio" do heading mesmo em alta confiança no movimento
- double fatorConfMovSemFix = 0.80; // penaliza confiança no movimento se não for RTK FIX
+ double distMin = 0.03;
+ double distFull = 0.25;
+ double pesoMinHeading = 0.30;
+ double fatorConfMovSemFix = 0.80;
- // Confiança em "movimento" por distância da janela (0..1)
- double fd = Smoothstep(0, 1, Norm(distLinearJanela, distMin, distFull));
- // Confiança por velocidade (0..1)
- double fv = velPercent / 100.0;
+ double fd = Smoothstep(
+ Norm(
+ distLinearJanela,
+ distMin,
+ distFull
+ )
+ );
+
+ double fv =
+ FuncoesMatematicas.Clamp(
+ velPercent / 100.0,
+ 0.0,
+ 1.0
+ );
double confMov = Math.Max(fd, fv);
- if (!rtkFix) confMov *= fatorConfMovSemFix;
- // Peso do heading = 1 - confMov, mas preservando contribuição mínima proporcional
+ if (!rtkFix)
+ confMov *= fatorConfMovSemFix;
+
double pesoHeading = 1.0 - confMov;
- double minHeading = pesoMinHeading * confMov;
- if (pesoHeading < minHeading) pesoHeading = minHeading;
- if (pesoHeading > 1.0) pesoHeading = 1.0;
+ double minHeading =
+ pesoMinHeading * confMov;
- double angMisto = MisturarAngulosCircular(headingDeg, angMovDeg, pesoHeading);
+ if (pesoHeading < minHeading)
+ pesoHeading = minHeading;
- if (!double.IsNaN(angFundidoAnterior) && alfaLowPass > 0)
- angMisto = LowPassAngle0to360(angFundidoAnterior, angMisto, alfaLowPass);
- else
- angMisto = Normalize0To360(angMisto);
+ if (pesoHeading > 1.0)
+ pesoHeading = 1.0;
- return angMisto;
+ double angMisto =
+ MisturarAngulosCircular(
+ headingDeg,
+ angMovDeg,
+ pesoHeading
+ );
- // ---------- helpers locais ----------
- double Norm(double v, double lo, double hi)
+ if (!double.IsNaN(angFundidoAnterior) &&
+ alfaLowPass > 0)
{
- if (hi <= lo) return v >= hi ? 1.0 : 0.0;
- double t = (v - lo) / (hi - lo);
- if (t < 0) t = 0; else if (t > 1) t = 1;
- return t;
- }
- double Smoothstep(double a, double b, double x)
- {
- // aqui x já normalizado 0..1 no uso acima
- x = FuncoesMatematicas.Clamp(x, 0.0, 1.0);
- return x * x * (3 - 2 * x);
- }
- double MisturarAngulosCircular(double aDeg, double bDeg, double pesoA /*0..1*/)
- {
- double a = aDeg * Math.PI / 180.0;
- double b = bDeg * Math.PI / 180.0;
- double x = Math.Cos(a) * pesoA + Math.Cos(b) * (1.0 - pesoA);
- double y = Math.Sin(a) * pesoA + Math.Sin(b) * (1.0 - pesoA);
- return Math.Atan2(y, x) * 180.0 / Math.PI;
- }
- double LowPassAngle(double atualDeg, double novoDeg, double alfa)
- {
- double diff = GPSUtils.NormalizarAngulo(novoDeg - atualDeg);
- return GPSUtils.NormalizarAngulo(atualDeg + alfa * diff);
- }
- double LowPassAngle0to360(double atualDeg, double novoDeg, double alfa)
- {
- double diff = NormalizeSigned180(novoDeg - atualDeg); // delta curto
- return Normalize0To360(atualDeg + alfa * diff);
- }
- double NormalizeSigned180(double angDeg)
- {
- angDeg = (angDeg + 180.0) % 360.0;
- if (angDeg < 0) angDeg += 360.0;
- return angDeg - 180.0; // [-180, +180)
- }
- double Normalize0To360(double angDeg)
- {
- angDeg %= 360.0;
- if (angDeg < 0) angDeg += 360.0;
- return angDeg; // [0, 360)
+ angMisto = LowPassAngle0to360(
+ angFundidoAnterior,
+ angMisto,
+ alfaLowPass
+ );
}
+
+ return Normalize0To360(angMisto);
+ }
+
+ private static double Norm(
+ double value,
+ double min,
+ double max)
+ {
+ if (max <= min)
+ return value >= max ? 1.0 : 0.0;
+
+ return FuncoesMatematicas.Clamp(
+ (value - min) / (max - min),
+ 0.0,
+ 1.0
+ );
+ }
+
+ private static double Smoothstep(double value)
+ {
+ double x =
+ FuncoesMatematicas.Clamp(
+ value,
+ 0.0,
+ 1.0
+ );
+
+ return x * x * (3.0 - 2.0 * x);
+ }
+
+ private static double MisturarAngulosCircular(
+ double aDeg,
+ double bDeg,
+ double pesoA)
+ {
+ double a = aDeg * Math.PI / 180.0;
+ double b = bDeg * Math.PI / 180.0;
+
+ double x =
+ Math.Cos(a) * pesoA +
+ Math.Cos(b) * (1.0 - pesoA);
+
+ double y =
+ Math.Sin(a) * pesoA +
+ Math.Sin(b) * (1.0 - pesoA);
+
+ return Math.Atan2(y, x) *
+ 180.0 / Math.PI;
+ }
+
+ private static double LowPassAngle0to360(
+ double atualDeg,
+ double novoDeg,
+ double alfa)
+ {
+ double diff =
+ NormalizeSigned180(
+ novoDeg - atualDeg
+ );
+
+ return Normalize0To360(
+ atualDeg + alfa * diff
+ );
+ }
+
+ private static double NormalizeSigned180(
+ double angDeg)
+ {
+ angDeg = (angDeg + 180.0) % 360.0;
+
+ if (angDeg < 0)
+ angDeg += 360.0;
+
+ return angDeg - 180.0;
+ }
+
+ private static double Normalize0To360(
+ double angDeg)
+ {
+ angDeg %= 360.0;
+
+ if (angDeg < 0)
+ angDeg += 360.0;
+
+ return angDeg;
}
public static void AtualizaDadosRedis()
{
try
{
- GPSModel posicaoAtual =
- Variaveis.OperacaoEmAndamento.Simulando
- ? historicoPosicao.Peek()
- : UltimaLeitura;
+ GPSModel posicaoAtual;
+
+ if (Variaveis.OperacaoEmAndamento.Simulando)
+ {
+ lock (_stateLock)
+ {
+ posicaoAtual =
+ historicoPosicao.Count > 0
+ ? historicoPosicao.Peek().Clone()
+ : UltimaLeitura.Clone();
+ }
+ }
+ else
+ {
+ posicaoAtual = GetSnapshot();
+ }
bool rtkValido =
posicaoAtual.IdadeCorrecao >= 0 &&
- posicaoAtual.IdadeCorrecao < rtk_timeout &&
+ posicaoAtual.IdadeCorrecao <
+ rtk_timeout &&
(
- posicaoAtual.QualidadeFix == TiposCorrecaoGPS.RTKFixo ||
- posicaoAtual.QualidadeFix == TiposCorrecaoGPS.RTKFlutuante ||
- posicaoAtual.QualidadeFix == TiposCorrecaoGPS.DGPS
+ posicaoAtual.QualidadeFix ==
+ TiposCorrecaoGPS.RTKFixo ||
+ posicaoAtual.QualidadeFix ==
+ TiposCorrecaoGPS.RTKFlutuante ||
+ posicaoAtual.QualidadeFix ==
+ TiposCorrecaoGPS.DGPS
);
- // ---------------------------------------------------------
- // ESTADO DO HEADING DUAL-ANTENNA
- // ---------------------------------------------------------
-
string statusOrientacao =
- (posicaoAtual.TipoOrientacao ?? string.Empty)
+ (posicaoAtual.TipoOrientacao ??
+ string.Empty)
.Trim()
.ToUpperInvariant();
double valorOrientacao =
posicaoAtual.OrientacaoReal;
- bool valorOrientacaoNumerico =
+ bool orientacaoValida =
+ statusOrientacao == "A" &&
!double.IsNaN(valorOrientacao) &&
- !double.IsInfinity(valorOrientacao);
-
- bool valorOrientacaoDentroDaFaixa =
- valorOrientacaoNumerico &&
+ !double.IsInfinity(valorOrientacao) &&
valorOrientacao >= 0.0 &&
valorOrientacao < 360.0;
- /*
- * A solução só é utilizável quando:
- *
- * 1. O UM982 informou status "A";
- * 2. O valor é numérico;
- * 3. O valor está na faixa válida de heading.
- *
- * Assim, a sentinela 9999 nunca será considerada válida.
- */
- bool orientacaoValida =
- statusOrientacao == "A" &&
- valorOrientacaoDentroDaFaixa;
+ GpsTransportMetrics metrics =
+ GetTransportMetrics();
RedisService.AtualizarCampos(
RedisService.ModKey(T_Code.Gps),
@@ -1553,77 +2859,840 @@ namespace AgroBase.Services
("conectado", Iniciado),
("freq_base", TaxaAmostragemHz),
- // Posição e orientação utilizada pelo carro
("lat", posicaoAtual.Latitude),
("lon", posicaoAtual.Longitude),
("theta", posicaoAtual.AnguloCarroDefinido),
- // Estado da solução GNSS
("fix", (int)posicaoAtual.QualidadeFix),
("rtk", rtkValido),
("hAcc", posicaoAtual.PrecisaoCm),
("nSatelites", posicaoAtual.NumeroSatelites),
("age", posicaoAtual.IdadeCorrecao),
- // Frequências
- ("freq.posicao", posicaoAtual.TimestampPos.frequencia),
- ("freq.orientacao", posicaoAtual.TimestampOri.frequencia),
+ ("freq.posicao",
+ posicaoAtual.TimestampPos.frequencia),
+ ("freq.orientacao",
+ posicaoAtual.TimestampOri.frequencia),
- // Latências
- ("latency.posicao", posicaoAtual.TimestampPos.dt),
- ("latency.orientacao", posicaoAtual.TimestampOri.dt),
+ ("latency.posicao",
+ posicaoAtual.TimestampPos.dt),
+ ("latency.orientacao",
+ posicaoAtual.TimestampOri.dt),
- // Timestamps monotônicos
- ("timestamp.posicao", posicaoAtual.TimestampPos.valor),
- ("timestamp.orientacao", posicaoAtual.TimestampOri.valor),
+ ("timestamp.posicao",
+ posicaoAtual.TimestampPos.valor),
+ ("timestamp.orientacao",
+ posicaoAtual.TimestampOri.valor),
- // Estado específico do heading dual-antenna
- ("orientacao.status", statusOrientacao),
- ("orientacao.valida", orientacaoValida),
- ("orientacao.valor", valorOrientacao),
+ ("orientacao.status",
+ statusOrientacao),
+ ("orientacao.valida",
+ orientacaoValida),
+ ("orientacao.valor",
+ valorOrientacao),
- ("heartbeat", posicaoAtual.Heartbeat)
+ ("heartbeat",
+ posicaoAtual.Heartbeat),
+
+ ("rtcm.received",
+ metrics.RtcmReceived),
+ ("rtcm.written",
+ metrics.RtcmWritten),
+ ("rtcm.queue_depth",
+ metrics.RtcmQueueDepth),
+ ("rtcm.last_rx_age_ms",
+ metrics.LastRtcmReceivedAgeMs),
+ ("rtcm.last_write_age_ms",
+ metrics.LastRtcmWrittenAgeMs),
+ ("rtcm.drop_stale",
+ metrics.RtcmDroppedStale),
+ ("rtcm.write_errors",
+ metrics.RtcmWriteErrors)
);
}
- catch (Exception e)
+ catch (Exception ex)
{
- Variaveis.MostrarLog(
- $"[GPSService.AtualizarDadosRedis] " +
- $"Erro ao salvar dados do GPS no Redis: {e.Message}"
+ RecordError(
+ "[GPSService.AtualizaDadosRedis] " +
+ ex.Message
);
- Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog(
- T_Code.Gps,
- StatusModulo.Falha,
- 0,
- $"Erro ao salvar dados do GPS no Redis: {e.Message}"
- );
+ try
+ {
+ Variaveis.OperacaoEmAndamento
+ .Sensoriamento
+ .InserirLog(
+ T_Code.Gps,
+ StatusModulo.Falha,
+ 0,
+ "Erro ao salvar dados do GPS no Redis: " +
+ ex.Message
+ );
+ }
+ catch { }
}
}
- public static Queue historicoPosicao = new Queue();
- public static void AtualizarAtrasoPosicoes(int errosConsiderar = 0)
+ public static void AtualizarAtrasoPosicoes(
+ int errosConsiderar = 0)
{
- historicoPosicao.Enqueue(UltimaLeitura);
- while (historicoPosicao.Count > 1 && (historicoPosicao.Count > errosConsiderar || errosConsiderar == 0))
+ lock (_stateLock)
{
- historicoPosicao.Dequeue();
+ historicoPosicao.Enqueue(
+ UltimaLeitura.Clone()
+ );
+
+ while (historicoPosicao.Count > 1 &&
+ (
+ historicoPosicao.Count >
+ errosConsiderar ||
+ errosConsiderar == 0
+ ))
+ {
+ historicoPosicao.Dequeue();
+ }
}
}
+ // ============================================================
+ // LOG EM WORKER
+ // ============================================================
+
+ private static void EnfileirarLogSnapshot()
+ {
+ try
+ {
+ string json;
+
+ lock (_stateLock)
+ {
+ GPSModel obj = UltimaLeitura.Clone();
+ obj.Momento = DateTime.Now;
+ json = JsonConvert.SerializeObject(obj);
+ }
+
+ _gpsLogQueue.Enqueue(json);
+
+ try { _logSignal.Release(); }
+ catch (SemaphoreFullException) { }
+ }
+ catch (Exception ex)
+ {
+ RecordError(
+ "[GPSService.EnfileirarLogSnapshot] " +
+ ex.Message
+ );
+ }
+ }
+
+ private static async Task LogWriterLoopAsync(
+ CancellationToken cancellationToken)
+ {
+ var lote = new List(300);
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ await _logSignal.WaitAsync(
+ cancellationToken
+ ).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+
+ string item;
+
+ while (lote.Count < 300 &&
+ _gpsLogQueue.TryDequeue(
+ out item))
+ {
+ lote.Add(item);
+ }
+
+ if (lote.Count < 300)
+ continue;
+
+ try
+ {
+ /*
+ * RegistrarLogDispositivo limpa a lista recebida.
+ * Passamos uma lista exclusiva do worker.
+ */
+ VariaveisOperacao
+ .RegistrarLogDispositivo(
+ lote,
+ T_Code.Gps,
+ ".json",
+ 300
+ );
+
+ lote = new List(300);
+ }
+ catch (Exception ex)
+ {
+ RecordError(
+ "[GPSService.LogWriter] " +
+ ex.Message
+ );
+ }
+ }
+
+ while (_gpsLogQueue.TryDequeue(out var pendente))
+ lote.Add(pendente);
+
+ if (lote.Count > 0)
+ {
+ try
+ {
+ VariaveisOperacao
+ .RegistrarLogDispositivo(
+ lote,
+ T_Code.Gps,
+ ".json",
+ 1
+ );
+ }
+ catch { }
+ }
+ }
+
+ // ============================================================
+ // WORKERS E SHUTDOWN
+ // ============================================================
+
+ private static void EnsureBackgroundWorkersStarted()
+ {
+ lock (_workerLock)
+ {
+ if (_serviceCts != null &&
+ !_serviceCts.IsCancellationRequested)
+ {
+ return;
+ }
+
+ _serviceCts =
+ new CancellationTokenSource();
+
+ CancellationToken token =
+ _serviceCts.Token;
+
+ _nmeaWorkerTask = Task.Run(
+ () => NmeaWorkerLoopAsync(token),
+ token
+ );
+
+ _rtcmWriterTask = Task.Run(
+ () => RtcmWriterLoopAsync(token),
+ token
+ );
+
+ _logWriterTask = Task.Run(
+ () => LogWriterLoopAsync(token),
+ token
+ );
+
+ _localPublisherTask = Task.Run(
+ () => LocalPublisherLoopAsync(token),
+ token
+ );
+ }
+ }
+
+ public static async Task EncerrarAsync()
+ {
+ await PararNtripAsync().ConfigureAwait(false);
+
+ CancellationTokenSource cts;
+ Task[] tasks;
+
+ lock (_workerLock)
+ {
+ cts = _serviceCts;
+
+ tasks = new[]
+ {
+ _nmeaWorkerTask,
+ _rtcmWriterTask,
+ _logWriterTask,
+ _localPublisherTask
+ }
+ .Where(x => x != null)
+ .ToArray();
+
+ _serviceCts = null;
+ _nmeaWorkerTask = null;
+ _rtcmWriterTask = null;
+ _logWriterTask = null;
+ _localPublisherTask = null;
+ }
+
+ if (cts != null)
+ {
+ try { cts.Cancel(); } catch { }
+ }
+
+ try
+ {
+ await Task.WhenAll(tasks)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) { }
+ catch (Exception ex)
+ {
+ RecordError(
+ "[GPSService.EncerrarAsync] " +
+ ex.Message
+ );
+ }
+
+ if (cts != null)
+ cts.Dispose();
+
+ CancellationTokenSource configCts =
+ Interlocked.Exchange(
+ ref _configurationCts,
+ null
+ );
+
+ if (configCts != null)
+ {
+ try { configCts.Cancel(); } catch { }
+ }
+
+ /*
+ * Aguarda uma configuração em andamento abandonar os locks.
+ * O token acima interrompe os delays cooperativos.
+ */
+ await _configurationLock.WaitAsync()
+ .ConfigureAwait(false);
+ _configurationLock.Release();
+
+ await _serialWriteLock.WaitAsync()
+ .ConfigureAwait(false);
+
+ try
+ {
+ lock (_portLifecycleLock)
+ {
+ SerialPort porta = PortaGPS;
+ PortaGPS = null;
+
+ if (porta != null)
+ {
+ try
+ {
+ porta.DataReceived -=
+ PortaGPS_DataReceived;
+ }
+ catch { }
+
+ try
+ {
+ if (porta.IsOpen)
+ porta.Close();
+ }
+ catch { }
+
+ try { porta.Dispose(); }
+ catch { }
+ }
+ }
+ }
+ finally
+ {
+ _serialWriteLock.Release();
+ }
+
+ if (configCts != null)
+ configCts.Dispose();
+ }
+
+ // ============================================================
+ // MÉTRICAS
+ // ============================================================
+
+ public static GpsTransportMetrics
+ GetTransportMetrics()
+ {
+ int queueDepth;
+ int nmeaQueueDepth;
+
+ lock (_rtcmQueueLock)
+ queueDepth = _rtcmQueue.Count;
+
+ lock (_nmeaQueueLock)
+ nmeaQueueDepth = _nmeaQueue.Count;
+
+ string lastError;
+
+ lock (_metricsTextLock)
+ lastError = _lastError;
+
+ return new GpsTransportMetrics
+ {
+ SerialConnected = Iniciado,
+ SerialPortName =
+ PortaGPS != null
+ ? PortaGPS.PortName
+ : null,
+
+ LastSerialRxAgeMs =
+ GetAgeMs(
+ Interlocked.Read(
+ ref _lastSerialRxMono
+ )
+ ),
+
+ LastValidNmeaAgeMs =
+ GetAgeMs(
+ Interlocked.Read(
+ ref _lastValidNmeaMono
+ )
+ ),
+
+ LastValidGgaAgeMs =
+ GetAgeMs(
+ Interlocked.Read(
+ ref _lastValidGgaMono
+ )
+ ),
+
+ LastValidThsAgeMs =
+ GetAgeMs(
+ Interlocked.Read(
+ ref _lastValidThsMono
+ )
+ ),
+
+ SerialBytesReceived =
+ Interlocked.Read(
+ ref _serialBytesReceived
+ ),
+
+ NmeaValid =
+ Interlocked.Read(ref _nmeaValid),
+
+ NmeaInvalid =
+ Interlocked.Read(ref _nmeaInvalid),
+
+ NmeaChecksumErrors =
+ Interlocked.Read(
+ ref _nmeaChecksumErrors
+ ),
+
+ NmeaBufferResets =
+ Interlocked.Read(
+ ref _nmeaBufferResets
+ ),
+
+ NmeaQueued =
+ Interlocked.Read(ref _nmeaQueued),
+
+ NmeaDroppedQueue =
+ Interlocked.Read(
+ ref _nmeaDroppedQueue
+ ),
+
+ NmeaQueueDepth = nmeaQueueDepth,
+
+ RtcmReceived =
+ Interlocked.Read(ref _rtcmReceived),
+
+ RtcmQueued =
+ Interlocked.Read(ref _rtcmQueued),
+
+ RtcmWritten =
+ Interlocked.Read(ref _rtcmWritten),
+
+ RtcmDroppedQueue =
+ Interlocked.Read(
+ ref _rtcmDroppedQueue
+ ),
+
+ RtcmDroppedStale =
+ Interlocked.Read(
+ ref _rtcmDroppedStale
+ ),
+
+ RtcmDroppedInvalid =
+ Interlocked.Read(
+ ref _rtcmDroppedInvalid
+ ),
+
+ RtcmWriteErrors =
+ Interlocked.Read(
+ ref _rtcmWriteErrors
+ ),
+
+ RtcmQueueDepth = queueDepth,
+
+ LastRtcmReceivedAgeMs =
+ GetAgeMs(
+ Interlocked.Read(
+ ref _lastRtcmReceivedMono
+ )
+ ),
+
+ LastRtcmWrittenAgeMs =
+ GetAgeMs(
+ Interlocked.Read(
+ ref _lastRtcmWrittenMono
+ )
+ ),
+
+ ConfigurationRuns =
+ Interlocked.Read(
+ ref _configurationRuns
+ ),
+
+ ConfigurationErrors =
+ Interlocked.Read(
+ ref _configurationErrors
+ ),
+
+ NtripRunning = LoopRTK_Ntrip,
+
+ NtripReconnects =
+ Interlocked.Read(
+ ref _ntripReconnects
+ ),
+
+ NtripBytesReceived =
+ Interlocked.Read(
+ ref _ntripBytesReceived
+ ),
+
+ NtripErrors =
+ Interlocked.Read(
+ ref _ntripErrors
+ ),
+
+ LastError = lastError
+ };
+ }
+
+ private static void RecordError(string error)
+ {
+ lock (_metricsTextLock)
+ _lastError = error;
+
+ Variaveis.MostrarLog(error);
+ }
+
+ // ============================================================
+ // HELPERS
+ // ============================================================
+
+ private static void WriteSerialUnsafe(
+ byte[] bytes,
+ int count)
+ {
+ SerialPort porta = PortaGPS;
+
+ if (porta == null || !porta.IsOpen)
+ throw new IOException(
+ "Porta GPS não está aberta."
+ );
+
+ porta.Write(bytes, 0, count);
+ }
+
+ private static double GetAgeMs(long timestamp)
+ {
+ if (timestamp <= 0)
+ return double.PositiveInfinity;
+
+ long delta =
+ Stopwatch.GetTimestamp() - timestamp;
+
+ if (delta <= 0)
+ return 0;
+
+ return delta * 1000.0 /
+ Stopwatch.Frequency;
+ }
+
+ private static string GetField(
+ string[] fields,
+ int index,
+ string fallback = "")
+ {
+ return fields != null &&
+ index >= 0 &&
+ index < fields.Length
+ ? fields[index] ?? fallback
+ : fallback;
+ }
+
+ private static string StripChecksum(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return string.Empty;
+
+ int index = value.IndexOf('*');
+
+ return index >= 0
+ ? value.Substring(0, index)
+ : value;
+ }
+
+ private static bool TryParseDmm(
+ string raw,
+ string hemisphere,
+ int degreeDigits,
+ out double result)
+ {
+ result = 0;
+
+ if (string.IsNullOrWhiteSpace(raw) ||
+ raw.Length <= degreeDigits)
+ {
+ return false;
+ }
+
+ double degrees;
+ double minutes;
+
+ if (!double.TryParse(
+ raw.Substring(0, degreeDigits),
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out degrees) ||
+ !double.TryParse(
+ raw.Substring(degreeDigits),
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out minutes))
+ {
+ return false;
+ }
+
+ result = degrees + minutes / 60.0;
+
+ if (string.Equals(
+ hemisphere,
+ "S",
+ StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(
+ hemisphere,
+ "W",
+ StringComparison.OrdinalIgnoreCase))
+ {
+ result = -result;
+ }
+
+ return !double.IsNaN(result) &&
+ !double.IsInfinity(result);
+ }
+
+ private static bool TryParseNmeaTime(
+ string raw,
+ out DateTime result)
+ {
+ result = default(DateTime);
+
+ if (string.IsNullOrWhiteSpace(raw) ||
+ raw.Length < 6)
+ {
+ return false;
+ }
+
+ int hh;
+ int mm;
+ double ss;
+
+ if (!int.TryParse(
+ raw.Substring(0, 2),
+ out hh) ||
+ !int.TryParse(
+ raw.Substring(2, 2),
+ out mm) ||
+ !double.TryParse(
+ raw.Substring(4),
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out ss))
+ {
+ return false;
+ }
+
+ int seconds = (int)Math.Floor(ss);
+ int milliseconds = (int)Math.Round(
+ (ss - seconds) * 1000.0
+ );
+
+ if (milliseconds >= 1000)
+ {
+ seconds++;
+ milliseconds = 0;
+ }
+
+ try
+ {
+ result = DateTime.UtcNow.Date
+ .AddHours(hh)
+ .AddMinutes(mm)
+ .AddSeconds(seconds)
+ .AddMilliseconds(milliseconds)
+ .ToLocalTime();
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static bool TryParseRmcDateTime(
+ string dateRaw,
+ string timeRaw,
+ out DateTime result)
+ {
+ result = default(DateTime);
+
+ if (string.IsNullOrWhiteSpace(dateRaw) ||
+ dateRaw.Length != 6 ||
+ string.IsNullOrWhiteSpace(timeRaw) ||
+ timeRaw.Length < 6)
+ {
+ return false;
+ }
+
+ int day;
+ int month;
+ int yearShort;
+ int hour;
+ int minute;
+ int second;
+
+ if (!int.TryParse(
+ dateRaw.Substring(0, 2),
+ out day) ||
+ !int.TryParse(
+ dateRaw.Substring(2, 2),
+ out month) ||
+ !int.TryParse(
+ dateRaw.Substring(4, 2),
+ out yearShort) ||
+ !int.TryParse(
+ timeRaw.Substring(0, 2),
+ out hour) ||
+ !int.TryParse(
+ timeRaw.Substring(2, 2),
+ out minute) ||
+ !int.TryParse(
+ timeRaw.Substring(4, 2),
+ out second))
+ {
+ return false;
+ }
+
+ try
+ {
+ result = new DateTime(
+ 2000 + yearShort,
+ month,
+ day,
+ hour,
+ minute,
+ second,
+ DateTimeKind.Utc
+ );
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private sealed class RtcmEnvelope
+ {
+ public byte[] Bytes { get; set; }
+ public long ReceivedMonotonic { get; set; }
+ public DateTime ReceivedUtc { get; set; }
+ public long Sequence { get; set; }
+ public int MessageType { get; set; }
+ public RtcmSource Source { get; set; }
+ }
+
+ private enum RtcmSource
+ {
+ Mqtt,
+ Ntrip
+ }
+
+ private sealed class NtripHeaderResult
+ {
+ public bool Success { get; set; }
+ public string StatusLine { get; set; }
+ public byte[] RemainingBytes { get; set; }
+ }
}
+ public sealed class GpsTransportMetrics
+ {
+ public bool SerialConnected { get; set; }
+ public string SerialPortName { get; set; }
+
+ public double LastSerialRxAgeMs { get; set; }
+ public double LastValidNmeaAgeMs { get; set; }
+ public double LastValidGgaAgeMs { get; set; }
+ public double LastValidThsAgeMs { get; set; }
+
+ public long SerialBytesReceived { get; set; }
+
+ public long NmeaValid { get; set; }
+ public long NmeaInvalid { get; set; }
+ public long NmeaChecksumErrors { get; set; }
+ public long NmeaBufferResets { get; set; }
+ public long NmeaQueued { get; set; }
+ public long NmeaDroppedQueue { get; set; }
+ public int NmeaQueueDepth { get; set; }
+
+ public long RtcmReceived { get; set; }
+ public long RtcmQueued { get; set; }
+ public long RtcmWritten { get; set; }
+ public long RtcmDroppedQueue { get; set; }
+ public long RtcmDroppedStale { get; set; }
+ public long RtcmDroppedInvalid { get; set; }
+ public long RtcmWriteErrors { get; set; }
+ public int RtcmQueueDepth { get; set; }
+ public double LastRtcmReceivedAgeMs { get; set; }
+ public double LastRtcmWrittenAgeMs { get; set; }
+
+ public long ConfigurationRuns { get; set; }
+ public long ConfigurationErrors { get; set; }
+
+ public bool NtripRunning { get; set; }
+ public long NtripReconnects { get; set; }
+ public long NtripBytesReceived { get; set; }
+ public long NtripErrors { get; set; }
+
+ public string LastError { get; set; }
+ }
public class GgaFix
{
- public DateTime TsUtc { get; }
- public double LatDeg { get; }
- public double LonDeg { get; }
- public double AltElipsoidalM { get; }
- public double HeadingDeg { get; }
- public TiposCorrecaoGPS FixQuality { get; }
+ public DateTime TsUtc { get; private set; }
+ public double LatDeg { get; private set; }
+ public double LonDeg { get; private set; }
+ public double AltElipsoidalM { get; private set; }
+ public double HeadingDeg { get; private set; }
+ public TiposCorrecaoGPS FixQuality { get; private set; }
- public GgaFix(DateTime tsUtc, double latDeg, double lonDeg, double altElipsoidalM, double headingDeg, TiposCorrecaoGPS fixQuality)
+ public GgaFix(
+ DateTime tsUtc,
+ double latDeg,
+ double lonDeg,
+ double altElipsoidalM,
+ double headingDeg,
+ TiposCorrecaoGPS fixQuality)
{
TsUtc = tsUtc;
LatDeg = latDeg;
@@ -1634,28 +3703,26 @@ namespace AgroBase.Services
}
}
-
-
public class GeoLeverArm
{
- // Lever arm físico fixo do equipamento
- private readonly double xFisico; // +frente (m)
- private readonly double yFisico; // +direita (m)
-
- // Offset operacional/de campo
- private readonly double xCampo; // +frente (m)
- private readonly double yCampo; // +direita (m)
+ private readonly double xFisico;
+ private readonly double yFisico;
+ private readonly double xCampo;
+ private readonly double yCampo;
private readonly bool invertHeading;
private readonly bool mirrorLateral;
- public GeoLeverArm(double offsetFisicoFrontalCm = 0, double offsetFisicoLateralCm = 0, double offsetCampoFrontalCm = 0, double offsetCampoLateralCm = 0, bool invH = false, bool mirrL = false)
+ public GeoLeverArm(
+ double offsetFisicoFrontalCm = 0,
+ double offsetFisicoLateralCm = 0,
+ double offsetCampoFrontalCm = 0,
+ double offsetCampoLateralCm = 0,
+ bool invH = false,
+ bool mirrL = false)
{
- // fixos do equipamento
xFisico = offsetFisicoFrontalCm / 100.0;
yFisico = offsetFisicoLateralCm / 100.0;
-
- // ajustes de campo
xCampo = offsetCampoFrontalCm / 100.0;
yCampo = offsetCampoLateralCm / 100.0;
@@ -1663,26 +3730,31 @@ namespace AgroBase.Services
mirrorLateral = mirrL;
}
- private double ToRad(double deg) => deg * Math.PI / 180.0;
-
- public double FrontalTotalCm => (xFisico + xCampo) * 100.0;
- public double LateralTotalCm => (yFisico + yCampo) * 100.0;
-
- ///
- /// Convenção:
- /// - Entrada = posição da antena GNSS
- /// - Saída = posição corrigida do centro/VRP
- /// - heading: 0° = Norte, 90° = Leste, sentido horário
- /// - frontal positivo = antena à frente do centro
- /// - lateral positivo = antena à direita do centro
- /// Consequência:
- /// - frontal positivo desloca a posição corrigida para trás do robô
- /// - lateral positivo desloca a posição corrigida para a esquerda do robô
- ///
- public (double lat, double lon) FixLeverArmLatLon_Fast(double latAnt_deg, double lonAnt_deg, double headingDeg, double headingFreq)
+ private static double ToRad(double deg)
{
- if (headingFreq < 1) return (latAnt_deg, lonAnt_deg);
-
+ return deg * Math.PI / 180.0;
+ }
+
+ public double FrontalTotalCm
+ {
+ get { return (xFisico + xCampo) * 100.0; }
+ }
+
+ public double LateralTotalCm
+ {
+ get { return (yFisico + yCampo) * 100.0; }
+ }
+
+ public (double lat, double lon)
+ FixLeverArmLatLon_Fast(
+ double latAnt_deg,
+ double lonAnt_deg,
+ double headingDeg,
+ double headingFreq)
+ {
+ if (headingFreq < 1)
+ return (latAnt_deg, lonAnt_deg);
+
double xTotal = xFisico + xCampo;
double yTotal = yFisico + yCampo;
@@ -1690,40 +3762,69 @@ namespace AgroBase.Services
yTotal = -yTotal;
double th = ToRad(headingDeg);
+
if (invertHeading)
th = -th;
- // Convenção NAV
- double fE = Math.Sin(th); // forward east
- double fN = Math.Cos(th); // forward north
+ double fE = Math.Sin(th);
+ double fN = Math.Cos(th);
- double rE = fN; // right east
- double rN = -fE; // right north
+ double rE = fN;
+ double rN = -fE;
- // deslocamento da antena no mundo
- double dE = xTotal * fE + yTotal * rE;
- double dN = xTotal * fN + yTotal * rN;
+ double dE =
+ xTotal * fE +
+ yTotal * rE;
+
+ double dN =
+ xTotal * fN +
+ yTotal * rN;
double latRad = ToRad(latAnt_deg);
- double dLat_deg = (dN / GPSUtils.RaioDaTerra) * 180.0 / Math.PI;
- double dLon_deg = (dE / (GPSUtils.RaioDaTerra * Math.Cos(latRad))) * 180.0 / Math.PI;
+ double dLatDeg =
+ dN / GPSUtils.RaioDaTerra *
+ 180.0 / Math.PI;
- // retorna o centro/VRP
- return (latAnt_deg - dLat_deg, lonAnt_deg - dLon_deg);
+ double cosLat = Math.Cos(latRad);
+
+ if (Math.Abs(cosLat) < 1e-12)
+ return (latAnt_deg, lonAnt_deg);
+
+ double dLonDeg =
+ dE /
+ (GPSUtils.RaioDaTerra * cosLat) *
+ 180.0 / Math.PI;
+
+ return (
+ latAnt_deg - dLatDeg,
+ lonAnt_deg - dLonDeg
+ );
}
- // lat/lon -> ENU (aproximação local, boa para ~km)
- public (double x, double y) GeodeticToENU(double lat, double lon, double lat0, double lon0)
+ public (double x, double y) GeodeticToENU(
+ double lat,
+ double lon,
+ double lat0,
+ double lon0)
{
- double latR = ToRad(lat), lonR = ToRad(lon);
- double lat0R = ToRad(lat0), lon0R = ToRad(lon0);
- double dLat = latR - lat0R, dLon = lonR - lon0R;
- double xEast = dLon * Math.Cos(lat0R) * GPSUtils.RaioDaTerra;
- double yNorth = dLat * GPSUtils.RaioDaTerra;
+ double latR = ToRad(lat);
+ double lonR = ToRad(lon);
+ double lat0R = ToRad(lat0);
+ double lon0R = ToRad(lon0);
+
+ double dLat = latR - lat0R;
+ double dLon = lonR - lon0R;
+
+ double xEast =
+ dLon *
+ Math.Cos(lat0R) *
+ GPSUtils.RaioDaTerra;
+
+ double yNorth =
+ dLat * GPSUtils.RaioDaTerra;
+
return (xEast, yNorth);
}
-
}
-
}
diff --git a/AgroBase/AgroBase/Services/MqttService.cs b/AgroBase/AgroBase/Services/MqttService.cs
index 32a8f4f15..033c67ca0 100644
--- a/AgroBase/AgroBase/Services/MqttService.cs
+++ b/AgroBase/AgroBase/Services/MqttService.cs
@@ -1,293 +1,1925 @@
-using AgroBase.Models;
+using AgroBase.Models;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Client.Options;
+using MQTTnet.Client.Disconnecting;
+using MQTTnet.Client.Subscribing;
+using MQTTnet.Client.Unsubscribing;
+using MQTTnet.Protocol;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
-public class MqttService
+///
+/// Cliente MQTT resiliente para uso em campo.
+///
+/// Garantias principais:
+/// - apenas uma tentativa de conexão por vez;
+/// - reconexão com backoff exponencial e jitter;
+/// - somente tópicos marcados para inscrição são inscritos;
+/// - callbacks não criam um Task.Run por mensagem;
+/// - cada tópico possui worker próprio, preservando ordem;
+/// - suporte a fila sequencial limitada, latest-only e callback inline explícito;
+/// - payload binário não é convertido para texto sem necessidade;
+/// - publicação não acumula mensagens quando o broker está desconectado;
+/// - ciclo de vida explícito com StartAsync, StopAsync e Dispose;
+/// - uma nova instância com o mesmo broker/client-id desarma a anterior.
+///
+/// A API antiga foi preservada para substituição direta no projeto.
+///
+public sealed class MqttService : IDisposable
{
- private IMqttClient _client;
- private IMqttClientOptions _options;
- private string _brokerAddr;
- private int _brokerPort;
- private AsyncTaskTimerModel tmrCheck;
+ private static readonly object _registryLock = new object();
+ private static readonly Dictionary _activeClients =
+ new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ private readonly object _topicsLock = new object();
+ private readonly object _stateLock = new object();
+ private readonly object _metricsLock = new object();
+ private readonly object _stopTaskLock = new object();
+
+ private readonly SemaphoreSlim _connectLock = new SemaphoreSlim(1, 1);
+ private readonly SemaphoreSlim _publishLock = new SemaphoreSlim(1, 1);
+
+ private readonly CancellationTokenSource _serviceCts = new CancellationTokenSource();
private readonly Action _logDebug;
+ private readonly Random _random;
- public List Topicos;
+ private readonly string _brokerAddr;
+ private readonly int _brokerPort;
+ private readonly string _clientId;
+ private readonly string _registryKey;
+ private readonly bool _local;
- public MqttService(string brokerAddress, int brokerPort, string client_id, bool local, Action logDebug)
+ private readonly IMqttClient _client;
+ private readonly IMqttClientOptions _options;
+ private readonly AsyncTaskTimerModel _reconnectTimer;
+
+ private int _started;
+ private int _stopping;
+ private int _disposed;
+ private int _manualDisconnect;
+ private Task _stopTask;
+
+ private long _nextReconnectTimestamp;
+ private double _reconnectBackoffSeconds = 1.0;
+
+ private const double ReconnectBackoffInitialSeconds = 1.0;
+ private const double ReconnectBackoffMaxSeconds = 30.0;
+ private const int ReconnectCheckIntervalMs = 500;
+ private const int ConnectTimeoutMs = 8000;
+ private const int PublishTimeoutMs = 5000;
+ private const int SubscribeTimeoutMs = 5000;
+
+ private long _connectionAttempts;
+ private long _connectionSuccesses;
+ private long _disconnects;
+ private long _reconnectSchedules;
+
+ private long _publishAttempts;
+ private long _publishSuccesses;
+ private long _publishFailures;
+ private long _publishDroppedDisconnected;
+ private long _publishTimeouts;
+ private long _publishedBytes;
+
+ private long _messagesReceived;
+ private long _receivedBytes;
+ private long _messagesWithoutTopic;
+ private long _callbackFailures;
+ private long _dispatchDropped;
+
+ private DateTime _lastConnectedUtc = DateTime.MinValue;
+ private DateTime _lastDisconnectedUtc = DateTime.MinValue;
+ private DateTime _lastPublishUtc = DateTime.MinValue;
+ private DateTime _lastReceiveUtc = DateTime.MinValue;
+ private DateTime _lastErrorUtc = DateTime.MinValue;
+
+ private string _lastError;
+ private double _lastPublishDurationMs;
+ private double _lastConnectDurationMs;
+
+ ///
+ /// Mantido público por compatibilidade com o código existente.
+ /// Para iteração interna, o serviço sempre usa snapshots protegidos.
+ /// Código externo deve evitar modificar esta lista durante callbacks.
+ ///
+ public List Topicos { get; private set; }
+
+ public string BrokerAddress { get { return _brokerAddr; } }
+ public int BrokerPort { get { return _brokerPort; } }
+ public string ClientId { get { return _clientId; } }
+ public bool Local { get { return _local; } }
+ public bool IsDisposed { get { return Volatile.Read(ref _disposed) == 1; } }
+ public bool IsStopping { get { return Volatile.Read(ref _stopping) == 1; } }
+
+ public MqttService(
+ string brokerAddress,
+ int brokerPort,
+ string client_id,
+ bool local,
+ Action logDebug)
{
- _logDebug = logDebug;
+ if (string.IsNullOrWhiteSpace(brokerAddress))
+ throw new ArgumentException("O endereço do broker MQTT é obrigatório.", nameof(brokerAddress));
- _brokerAddr = brokerAddress;
+ if (brokerPort <= 0 || brokerPort > 65535)
+ throw new ArgumentOutOfRangeException(nameof(brokerPort));
+
+ if (string.IsNullOrWhiteSpace(client_id))
+ throw new ArgumentException("O client_id MQTT é obrigatório.", nameof(client_id));
+
+ _logDebug = logDebug ?? (_ => { });
+ _brokerAddr = brokerAddress.Trim();
_brokerPort = brokerPort;
+ _local = local;
+ _clientId = local ? client_id + "_local" : client_id;
+ _registryKey = _brokerAddr + ":" + _brokerPort + "/" + _clientId;
+ _random = new Random(unchecked(Environment.TickCount * 397) ^ _registryKey.GetHashCode());
+
+ Topicos = new List();
var factory = new MqttFactory();
_client = factory.CreateMqttClient();
- string client = !local ? client_id : (client_id + "_local");
-
_options = new MqttClientOptionsBuilder()
- .WithClientId(client)
+ .WithClientId(_clientId)
.WithTcpServer(_brokerAddr, _brokerPort)
.WithCleanSession()
+ .WithKeepAlivePeriod(TimeSpan.FromSeconds(15))
+ .WithCommunicationTimeout(TimeSpan.FromMilliseconds(ConnectTimeoutMs))
.Build();
- _client.UseConnectedHandler(async e =>
- {
- _logDebug("Connected to MQTT Broker.");
- if (Topicos?.Any(x => x.Inscrever) ?? false)
- {
- foreach (var topic in Topicos)
- {
- await SubscribeAsync(topic);
- }
- }
- });
+ ConfigureHandlers();
+ RegisterAsActiveClient();
- _client.UseDisconnectedHandler(e =>
- {
- _logDebug("Disconnected from MQTT Broker.");
- });
+ _reconnectTimer = new AsyncTaskTimerModel(
+ "mqtt-reconnect-" + SanitizeTimerId(_clientId) + "@" + SanitizeTimerId(_brokerAddr) + "-" + _brokerPort,
+ ReconnectTickAsync,
+ ReconnectCheckIntervalMs,
+ timeout: ConnectTimeoutMs + 2000,
+ scheduleMode: AsyncTaskTimerScheduleMode.FixedRateSkipMissed,
+ runImmediately: true);
- _client.UseApplicationMessageReceivedHandler(e =>
- {
- var payloadBytes = e.ApplicationMessage.Payload ?? Array.Empty();
- var messageText = string.Empty;
+ _reconnectTimer.DebugMessages = false;
- // Só tenta decodificar texto se for realmente necessário
- try { messageText = Encoding.UTF8.GetString(payloadBytes); } catch { /* binário puro */ }
-
- //Console.WriteLine($"Message received on topic {e.ApplicationMessage.Topic}");
- // Aqui você pode adicionar o código para lidar com a mensagem recebida
-
- MqttTopicosModel topico = Topicos.FirstOrDefault(x => x.Topico == e.ApplicationMessage.Topic);
-
- if (topico != null)
- {
- var Mensagem = new MqttTopicosMensagensModel()
- {
- Momento = DateTime.Now,
- Cliente = e.ClientId,
- Mensagem = messageText,
- Bytes = payloadBytes
- };
- topico.Mensagens.Add(Mensagem);
-
- // Limitar o número de mensagens mantidas, removendo as mais antigas se necessário
- while (topico.Mensagens.Count > topico.MensagensManter)
- {
- topico.Mensagens.RemoveAt(0); // Remove a mensagem mais antiga (index 0)
- }
-
- if (topico.Callback != null)
- {
- _ = Task.Run(async () =>
- {
- try
- {
- await topico.Callback(Mensagem); // await real e funcional
- }
- catch (Exception ex)
- {
- _logDebug($"Erro no callback do tópico '{topico.Topico}': {ex}");
- }
- });
- }
- }
- });
-
- Topicos = new List();
-
- tmrCheck?.Dispose();
- tmrCheck = new AsyncTaskTimerModel("tmrCheck", tmrCheck_Tick, 1000);
- tmrCheck.Start();
+ // Compatibilidade: o serviço antigo começava a tentar conectar no construtor.
+ Volatile.Write(ref _started, 1);
+ _reconnectTimer.Start();
}
- private async Task tmrCheck_Tick()
+ private void ConfigureHandlers()
{
- if (!_client.IsConnected)
+ _client.UseConnectedHandler(async e =>
{
- await ConnectAsync();
+ await HandleConnectedAsync().ConfigureAwait(false);
+ });
+
+ _client.UseDisconnectedHandler(async e =>
+ {
+ await HandleDisconnectedAsync(e).ConfigureAwait(false);
+ });
+
+ _client.UseApplicationMessageReceivedHandler(async e =>
+ {
+ await HandleApplicationMessageAsync(e).ConfigureAwait(false);
+ });
+ }
+
+ private void RegisterAsActiveClient()
+ {
+ MqttService previous = null;
+
+ lock (_registryLock)
+ {
+ if (_activeClients.TryGetValue(_registryKey, out previous) &&
+ !ReferenceEquals(previous, this))
+ {
+ _activeClients[_registryKey] = this;
+ }
+ else
+ {
+ _activeClients[_registryKey] = this;
+ previous = null;
+ }
}
+
+ if (previous != null)
+ {
+ previous.RequestSuperseded();
+ SafeLog("Uma instância MQTT anterior com o mesmo broker/client-id foi desarmada.");
+ }
+ }
+
+ private void RequestSuperseded()
+ {
+ Volatile.Write(ref _started, 0);
+ Volatile.Write(ref _manualDisconnect, 1);
+
+ try { _reconnectTimer?.Stop(); } catch { }
+
+ // Uma única tarefa encerra a instância substituída. O registry já aponta
+ // para a nova instância, portanto a antiga não deve remover a chave.
+ Task stopTask = GetOrCreateStopTask(removeFromRegistry: false);
+
+ _ = stopTask.ContinueWith(t =>
+ {
+ if (t.IsFaulted && t.Exception != null)
+ SafeLog("Erro ao encerrar instância MQTT substituída: " + t.Exception.GetBaseException().Message);
+ }, TaskScheduler.Default);
+ }
+
+ public async Task StartAsync()
+ {
+ ThrowIfDisposed();
+
+ Volatile.Write(ref _manualDisconnect, 0);
+ Volatile.Write(ref _stopping, 0);
+ Volatile.Write(ref _started, 1);
+
+ if (!_reconnectTimer.IsRunning)
+ _reconnectTimer.Start();
+
+ await ConnectAsync().ConfigureAwait(false);
}
public bool StatusConexao()
{
- return _client.IsConnected;
+ return !IsDisposed && _client.IsConnected;
}
public async Task ConnectAsync()
{
+ ThrowIfDisposed();
+
+ Volatile.Write(ref _manualDisconnect, 0);
+
+ if (IsStopping || Volatile.Read(ref _started) == 0)
+ return;
+
+ await ConnectInternalAsync(_serviceCts.Token).ConfigureAwait(false);
+ }
+
+ private async Task ConnectInternalAsync(CancellationToken cancellationToken)
+ {
+ if (_client.IsConnected)
+ return true;
+
+ if (IsDisposed || IsStopping || Volatile.Read(ref _manualDisconnect) == 1)
+ return false;
+
+ bool lockTaken = false;
+ long started = Stopwatch.GetTimestamp();
+
try
{
- await _client.ConnectAsync(_options);
+ await _connectLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ lockTaken = true;
+
+ if (_client.IsConnected)
+ return true;
+
+ if (IsDisposed || IsStopping || Volatile.Read(ref _manualDisconnect) == 1)
+ return false;
+
+ Interlocked.Increment(ref _connectionAttempts);
+ SafeLog("Conectando ao broker MQTT " + _brokerAddr + ":" + _brokerPort + "...");
+
+ using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
+ {
+ timeoutCts.CancelAfter(ConnectTimeoutMs);
+
+ try
+ {
+ await _client.ConnectAsync(_options, timeoutCts.Token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
+ {
+ RegisterError("Timeout ao conectar ao broker MQTT.");
+ ScheduleReconnect(increaseBackoff: true);
+ return false;
+ }
+ }
+
+ return _client.IsConnected;
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ return false;
}
catch (Exception ex)
{
- _logDebug("Erro ao tentar se conectar ao MQTT Broker: " + ex.Message);
+ RegisterError("Erro ao conectar ao broker MQTT: " + ex.Message);
+ ScheduleReconnect(increaseBackoff: true);
+ return false;
}
+ finally
+ {
+ double elapsed = ElapsedMilliseconds(started);
+ lock (_metricsLock)
+ {
+ _lastConnectDurationMs = elapsed;
+ }
+
+ if (lockTaken)
+ _connectLock.Release();
+ }
+ }
+
+ private async Task ReconnectTickAsync(CancellationToken cancellationToken)
+ {
+ if (IsDisposed || IsStopping || Volatile.Read(ref _started) == 0)
+ return;
+
+ if (Volatile.Read(ref _manualDisconnect) == 1)
+ return;
+
+ if (_client.IsConnected)
+ return;
+
+ long next = Interlocked.Read(ref _nextReconnectTimestamp);
+ long now = Stopwatch.GetTimestamp();
+
+ if (next > 0 && now < next)
+ return;
+
+ await ConnectInternalAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task HandleConnectedAsync()
+ {
+ if (IsDisposed || IsStopping)
+ return;
+
+ Interlocked.Increment(ref _connectionSuccesses);
+
+ lock (_metricsLock)
+ {
+ _lastConnectedUtc = DateTime.UtcNow;
+ _lastError = null;
+ }
+
+ lock (_stateLock)
+ {
+ _reconnectBackoffSeconds = ReconnectBackoffInitialSeconds;
+ Interlocked.Exchange(ref _nextReconnectTimestamp, 0L);
+ }
+
+ SafeLog("Conectado ao MQTT Broker.");
+
+ MqttTopicosModel[] subscriptions = GetTopicsSnapshot()
+ .Where(x => x != null && x.Inscrever && x.Habilitado)
+ .ToArray();
+
+ foreach (MqttTopicosModel topic in subscriptions)
+ {
+ topic.MarcarNaoInscrito();
+ }
+
+ foreach (MqttTopicosModel topic in subscriptions)
+ {
+ if (IsDisposed || IsStopping || !_client.IsConnected)
+ break;
+
+ await SubscribeInternalAsync(topic, _serviceCts.Token).ConfigureAwait(false);
+ }
+ }
+
+ private Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs e)
+ {
+ Interlocked.Increment(ref _disconnects);
+
+ lock (_metricsLock)
+ {
+ _lastDisconnectedUtc = DateTime.UtcNow;
+ }
+
+ foreach (MqttTopicosModel topic in GetTopicsSnapshot())
+ {
+ topic?.MarcarNaoInscrito();
+ }
+
+ string reason = e == null
+ ? "motivo não informado"
+ : (e.Exception != null ? e.Exception.Message : e.Reason.ToString());
+
+ SafeLog("Desconectado do MQTT Broker: " + reason);
+
+ if (!IsDisposed &&
+ !IsStopping &&
+ Volatile.Read(ref _started) == 1 &&
+ Volatile.Read(ref _manualDisconnect) == 0)
+ {
+ ScheduleReconnect(increaseBackoff: true);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private void ScheduleReconnect(bool increaseBackoff)
+ {
+ if (IsDisposed || IsStopping || Volatile.Read(ref _manualDisconnect) == 1)
+ return;
+
+ double delaySeconds;
+
+ lock (_stateLock)
+ {
+ double current = Math.Max(ReconnectBackoffInitialSeconds, _reconnectBackoffSeconds);
+ double jitter = 0.85 + (_random.NextDouble() * 0.30); // 85% a 115%
+ delaySeconds = Math.Min(ReconnectBackoffMaxSeconds, current * jitter);
+
+ if (increaseBackoff)
+ {
+ _reconnectBackoffSeconds = Math.Min(
+ ReconnectBackoffMaxSeconds,
+ Math.Max(ReconnectBackoffInitialSeconds, current * 1.8));
+ }
+ }
+
+ long due = Stopwatch.GetTimestamp() + MillisecondsToStopwatchTicks(delaySeconds * 1000.0);
+ Interlocked.Exchange(ref _nextReconnectTimestamp, due);
+ Interlocked.Increment(ref _reconnectSchedules);
}
public async Task DisconnectAsync()
{
- await _client.DisconnectAsync();
+ if (IsDisposed)
+ return;
+
+ Volatile.Write(ref _manualDisconnect, 1);
+ Interlocked.Exchange(ref _nextReconnectTimestamp, 0L);
+
+ if (!_client.IsConnected)
+ return;
+
+ try
+ {
+ await _client.DisconnectAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ RegisterError("Erro ao desconectar MQTT: " + ex.Message);
+ }
}
- public async Task PublishAsync(MqttTopicosModel topic, string message, bool Forcar = false)
+ public async Task StopAsync()
{
- if (topic == null || (topic.Inscrever && !Forcar))
+ await GetOrCreateStopTask(removeFromRegistry: true).ConfigureAwait(false);
+ }
+
+ private Task GetOrCreateStopTask(bool removeFromRegistry)
+ {
+ lock (_stopTaskLock)
{
- return;
+ if (_stopTask == null)
+ _stopTask = StopCoreAsync(removeFromRegistry);
+
+ return _stopTask;
+ }
+ }
+
+ private async Task StopCoreAsync(bool removeFromRegistry)
+ {
+ Interlocked.Exchange(ref _stopping, 1);
+ Volatile.Write(ref _started, 0);
+ Volatile.Write(ref _manualDisconnect, 1);
+
+ try { _serviceCts.Cancel(); } catch { }
+
+ try
+ {
+ await _reconnectTimer.StopAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ SafeLog("Erro ao parar timer de reconexão MQTT: " + ex.Message);
+ }
+
+ MqttTopicosModel[] topics = GetTopicsSnapshot();
+ foreach (MqttTopicosModel topic in topics)
+ {
+ if (topic != null)
+ await topic.PararWorkerAsync().ConfigureAwait(false);
}
if (_client.IsConnected)
{
try
{
- var payload = Encoding.UTF8.GetBytes(message);
- var mqttMessage = new MqttApplicationMessageBuilder()
- .WithTopic(topic.Topico)
- .WithPayload(payload)
- .Build();
-
- await _client.PublishAsync(mqttMessage);
+ await _client.DisconnectAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
- _logDebug(ex.Message);
+ SafeLog("Erro ao desconectar MQTT durante encerramento: " + ex.Message);
+ }
+ }
+
+ if (removeFromRegistry)
+ {
+ lock (_registryLock)
+ {
+ MqttService current;
+ if (_activeClients.TryGetValue(_registryKey, out current) &&
+ ReferenceEquals(current, this))
+ {
+ _activeClients.Remove(_registryKey);
+ }
}
}
}
+ // -----------------------------------------------------------------
+ // PUBLICAÇÃO
+ // -----------------------------------------------------------------
+
+ ///
+ /// Assinatura antiga preservada. Para obter o resultado detalhado,
+ /// use PublishWithResultAsync.
+ ///
+ public async Task PublishAsync(MqttTopicosModel topic, string message, bool Forcar = false)
+ {
+ await PublishWithResultAsync(topic, message, Forcar).ConfigureAwait(false);
+ }
+
+ ///
+ /// Assinatura antiga preservada. Para obter o resultado detalhado,
+ /// use PublishWithResultAsync.
+ ///
public async Task PublishAsync(MqttTopicosModel topic, byte[] payloadBytes, bool Forcar = false)
{
- if (topic.Inscrever || (topic.Inscrever && !Forcar))
+ await PublishWithResultAsync(topic, payloadBytes, Forcar).ConfigureAwait(false);
+ }
+
+ public Task PublishWithResultAsync(
+ MqttTopicosModel topic,
+ string message,
+ bool forcar = false,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ byte[] payload = Encoding.UTF8.GetBytes(message ?? string.Empty);
+ return PublishCoreAsync(topic, payload, forcar, cancellationToken);
+ }
+
+ public Task PublishWithResultAsync(
+ MqttTopicosModel topic,
+ byte[] payloadBytes,
+ bool forcar = false,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return PublishCoreAsync(
+ topic,
+ payloadBytes ?? Array.Empty(),
+ forcar,
+ cancellationToken);
+ }
+
+ private async Task PublishCoreAsync(
+ MqttTopicosModel topic,
+ byte[] payloadBytes,
+ bool forcar,
+ CancellationToken cancellationToken)
+ {
+ Interlocked.Increment(ref _publishAttempts);
+
+ if (topic == null)
+ return MqttPublishResult.Failure(MqttPublishStatus.InvalidTopic, "Tópico MQTT nulo.");
+
+ if (string.IsNullOrWhiteSpace(topic.Topico))
+ return MqttPublishResult.Failure(MqttPublishStatus.InvalidTopic, "Nome do tópico MQTT vazio.");
+
+ if (!topic.Habilitado)
+ return MqttPublishResult.Failure(MqttPublishStatus.TopicDisabled, "Tópico desabilitado.");
+
+ if (topic.Inscrever && !forcar)
+ return MqttPublishResult.Failure(MqttPublishStatus.PublishNotAllowed, "O tópico foi configurado somente para inscrição.");
+
+ if (IsDisposed || IsStopping)
+ return MqttPublishResult.Failure(MqttPublishStatus.ServiceStopping, "Serviço MQTT encerrando.");
+
+ if (!_client.IsConnected)
{
- return;
+ Interlocked.Increment(ref _publishDroppedDisconnected);
+ return MqttPublishResult.Failure(MqttPublishStatus.Disconnected, "Cliente MQTT desconectado.");
}
- if (_client.IsConnected)
+ long started = Stopwatch.GetTimestamp();
+ bool gateTaken = false;
+
+ try
{
- try
+ using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
+ cancellationToken,
+ _serviceCts.Token))
{
+ linkedCts.CancelAfter(PublishTimeoutMs);
+ await _publishLock.WaitAsync(linkedCts.Token).ConfigureAwait(false);
+ gateTaken = true;
+
+ if (!_client.IsConnected)
+ {
+ Interlocked.Increment(ref _publishDroppedDisconnected);
+ return MqttPublishResult.Failure(MqttPublishStatus.Disconnected, "Cliente MQTT desconectou antes da publicação.");
+ }
+
var mqttMessage = new MqttApplicationMessageBuilder()
.WithTopic(topic.Topico)
.WithPayload(payloadBytes)
+ .WithQualityOfServiceLevel(ToMqttNetQos(topic.QoS))
+ .WithRetainFlag(topic.Retain)
.Build();
- await _client.PublishAsync(mqttMessage);
+ await _client.PublishAsync(mqttMessage, linkedCts.Token).ConfigureAwait(false);
}
- catch (Exception ex)
+
+ double elapsed = ElapsedMilliseconds(started);
+ Interlocked.Increment(ref _publishSuccesses);
+ Interlocked.Add(ref _publishedBytes, payloadBytes.LongLength);
+
+ lock (_metricsLock)
{
- _logDebug(ex.Message);
+ _lastPublishUtc = DateTime.UtcNow;
+ _lastPublishDurationMs = elapsed;
+ }
+
+ topic.RegistrarPublicacao(payloadBytes.LongLength, elapsed, true, null);
+
+ return MqttPublishResult.Success(payloadBytes.Length, elapsed);
+ }
+ catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested && !_serviceCts.IsCancellationRequested)
+ {
+ double elapsed = ElapsedMilliseconds(started);
+ Interlocked.Increment(ref _publishTimeouts);
+ Interlocked.Increment(ref _publishFailures);
+
+ string error = "Timeout ao publicar no tópico '" + topic.Topico + "'.";
+ RegisterError(error);
+ topic.RegistrarPublicacao(payloadBytes.LongLength, elapsed, false, error);
+
+ return MqttPublishResult.Failure(MqttPublishStatus.Timeout, error, payloadBytes.Length, elapsed);
+ }
+ catch (OperationCanceledException)
+ {
+ double elapsed = ElapsedMilliseconds(started);
+ Interlocked.Increment(ref _publishFailures);
+ return MqttPublishResult.Failure(MqttPublishStatus.Canceled, "Publicação cancelada.", payloadBytes.Length, elapsed);
+ }
+ catch (Exception ex)
+ {
+ double elapsed = ElapsedMilliseconds(started);
+ Interlocked.Increment(ref _publishFailures);
+
+ string error = "Erro ao publicar no tópico '" + topic.Topico + "': " + ex.Message;
+ RegisterError(error);
+ topic.RegistrarPublicacao(payloadBytes.LongLength, elapsed, false, error);
+
+ return MqttPublishResult.Failure(MqttPublishStatus.Error, error, payloadBytes.Length, elapsed);
+ }
+ finally
+ {
+ if (gateTaken)
+ _publishLock.Release();
+ }
+ }
+
+ private static MqttQualityOfServiceLevel ToMqttNetQos(MqttQosLevel qos)
+ {
+ switch (qos)
+ {
+ case MqttQosLevel.AtLeastOnce:
+ return MqttQualityOfServiceLevel.AtLeastOnce;
+
+ case MqttQosLevel.ExactlyOnce:
+ return MqttQualityOfServiceLevel.ExactlyOnce;
+
+ default:
+ return MqttQualityOfServiceLevel.AtMostOnce;
+ }
+ }
+
+ private static MqttQosLevel FromMqttNetQos(MqttQualityOfServiceLevel qos)
+ {
+ switch (qos)
+ {
+ case MqttQualityOfServiceLevel.AtLeastOnce:
+ return MqttQosLevel.AtLeastOnce;
+
+ case MqttQualityOfServiceLevel.ExactlyOnce:
+ return MqttQosLevel.ExactlyOnce;
+
+ default:
+ return MqttQosLevel.AtMostOnce;
+ }
+ }
+
+ // -----------------------------------------------------------------
+ // TÓPICOS / INSCRIÇÃO
+ // -----------------------------------------------------------------
+
+ public async Task AdicionarNovoTopico(
+ string topico,
+ bool inscrever = false,
+ int mensagensManter = 2,
+ Func callback = null)
+ {
+ return await AdicionarNovoTopico(
+ topico,
+ inscrever,
+ mensagensManter,
+ callback,
+ MqttPayloadType.Auto,
+ MqttDispatchMode.Sequential,
+ queueCapacity: 64,
+ overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
+ qos: MqttQosLevel.AtMostOnce,
+ retain: false).ConfigureAwait(false);
+ }
+
+ public async Task AdicionarNovoTopico(
+ string topico,
+ bool inscrever,
+ int mensagensManter,
+ Func callback,
+ MqttPayloadType payloadType,
+ MqttDispatchMode dispatchMode,
+ int queueCapacity = 64,
+ MqttQueueOverflowPolicy overflowPolicy = MqttQueueOverflowPolicy.DropOldest,
+ MqttQosLevel qos = MqttQosLevel.AtMostOnce,
+ bool retain = false)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrWhiteSpace(topico))
+ throw new ArgumentException("O nome do tópico MQTT é obrigatório.", nameof(topico));
+
+ if (mensagensManter < 0)
+ throw new ArgumentOutOfRangeException(nameof(mensagensManter));
+
+ if (queueCapacity <= 0)
+ throw new ArgumentOutOfRangeException(nameof(queueCapacity));
+
+ MqttTopicosModel topic;
+
+ lock (_topicsLock)
+ {
+ Topicos.RemoveAll(x => x == null);
+
+ topic = Topicos.FirstOrDefault(x =>
+ string.Equals(x.Topico, topico, StringComparison.Ordinal));
+
+ if (topic == null)
+ {
+ topic = new MqttTopicosModel(
+ owner: this,
+ serviceToken: _serviceCts.Token)
+ {
+ Topico = topico,
+ Inscrever = inscrever,
+ MensagensManter = mensagensManter,
+ Callback = callback,
+ PayloadType = payloadType,
+ DispatchMode = dispatchMode,
+ QueueCapacity = queueCapacity,
+ OverflowPolicy = overflowPolicy,
+ QoS = qos,
+ Retain = retain,
+ Habilitado = true,
+ };
+
+ Topicos.Add(topic);
+ }
+ else
+ {
+ topic.AtualizarConfiguracao(
+ inscrever,
+ mensagensManter,
+ callback,
+ payloadType,
+ dispatchMode,
+ queueCapacity,
+ overflowPolicy,
+ qos,
+ retain);
}
}
+
+ topic.GarantirWorkerIniciado();
+
+ if (topic.Inscrever && _client.IsConnected)
+ await SubscribeInternalAsync(topic, _serviceCts.Token).ConfigureAwait(false);
+
+ // Diferente da versão antiga, cadastrar um tópico não apaga retained.
+ // A limpeza agora é sempre uma ação explícita via LimparMensagensRetidas.
+ return topic;
}
public async Task SubscribeAsync(MqttTopicosModel topic)
{
+ if (topic == null || !topic.Inscrever || !topic.Habilitado)
+ return;
+
+ if (!_client.IsConnected)
+ await ConnectAsync().ConfigureAwait(false);
+
+ if (_client.IsConnected)
+ await SubscribeInternalAsync(topic, _serviceCts.Token).ConfigureAwait(false);
+ }
+
+ private async Task SubscribeInternalAsync(
+ MqttTopicosModel topic,
+ CancellationToken cancellationToken)
+ {
+ if (topic == null || !topic.Inscrever || !topic.Habilitado)
+ return false;
+
+ if (!_client.IsConnected || IsDisposed || IsStopping)
+ return false;
+
+ if (!await topic.EntrarInscricaoAsync(cancellationToken).ConfigureAwait(false))
+ return topic.Inscrito;
+
try
{
- if (!_client.IsConnected)
+ if (topic.Inscrito)
+ return true;
+
+ using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(
+ cancellationToken,
+ _serviceCts.Token))
{
- await ConnectAsync();
+ timeoutCts.CancelAfter(SubscribeTimeoutMs);
+
+ var filter = new TopicFilterBuilder()
+ .WithTopic(topic.Topico)
+ .WithQualityOfServiceLevel(ToMqttNetQos(topic.QoS))
+ .Build();
+
+ var subscribeOptions = new MqttClientSubscribeOptionsBuilder()
+ .WithTopicFilter(filter)
+ .Build();
+
+ await _client.SubscribeAsync(
+ subscribeOptions,
+ timeoutCts.Token
+ ).ConfigureAwait(false);
}
- await _client.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic.Topico).Build());
- _logDebug($"Subscribed to topic: {topic.Topico}");
+
+ topic.MarcarInscrito();
+ SafeLog("Inscrito no tópico: " + topic.Topico);
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ topic.MarcarNaoInscrito();
+ return false;
}
catch (Exception ex)
{
- _logDebug($"An error occurred while subscribing from the topic: {ex.Message}");
+ topic.MarcarNaoInscrito();
+ RegisterError("Erro ao inscrever no tópico '" + topic.Topico + "': " + ex.Message);
+ return false;
+ }
+ finally
+ {
+ topic.SairInscricao();
}
}
public async Task UnsubscribeAsync(MqttTopicosModel topic)
{
if (topic == null || !topic.Inscrever)
- {
return;
- }
+
+ topic.MarcarNaoInscrito();
+
+ if (!_client.IsConnected)
+ return;
+
try
{
- if (!_client.IsConnected)
+ using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(_serviceCts.Token))
{
- await ConnectAsync();
+ timeoutCts.CancelAfter(SubscribeTimeoutMs);
+
+ var unsubscribeOptions = new MqttClientUnsubscribeOptionsBuilder()
+ .WithTopicFilter(topic.Topico)
+ .Build();
+
+ await _client.UnsubscribeAsync(
+ unsubscribeOptions,
+ timeoutCts.Token
+ ).ConfigureAwait(false);
}
- await _client.UnsubscribeAsync(new string[] { topic.Topico });
- _logDebug($"Unsubscribed from topic: {topic.Topico}");
+
+ SafeLog("Desinscrito do tópico: " + topic.Topico);
+ }
+ catch (OperationCanceledException)
+ {
}
catch (Exception ex)
{
- _logDebug($"An error occurred while unsubscribing from the topic: {ex.Message}");
+ RegisterError("Erro ao desinscrever do tópico '" + topic.Topico + "': " + ex.Message);
}
}
- public async Task AdicionarNovoTopico(string topico, bool inscrever = false, int mensagensManter = 2, Func callback = null)
- {
- MqttTopicosModel _mqttTopico = null;
-
- Topicos.Remove(null);
-
- _mqttTopico = Topicos.FirstOrDefault(x => x.Topico == topico);
-
- if (_mqttTopico == null)
- {
- _mqttTopico = new MqttTopicosModel()
- {
- Topico = topico,
- Mensagens = new List(),
- Inscrever = inscrever,
- MensagensManter = mensagensManter,
- Callback = callback
- };
-
- Topicos.Add(_mqttTopico);
-
- if (_mqttTopico.Inscrever)
- {
- await SubscribeAsync(_mqttTopico);
- }
-
- await LimparMensagensRetidas(_mqttTopico.Topico);
- }
-
- return _mqttTopico;
- }
-
public async Task LimparMensagensRetidas(string topico)
{
- if (_client.IsConnected)
+ if (string.IsNullOrWhiteSpace(topico))
+ throw new ArgumentException("O tópico é obrigatório.", nameof(topico));
+
+ if (!_client.IsConnected)
+ {
+ SafeLog("Não foi possível limpar retained de '" + topico + "': cliente desconectado.");
+ return;
+ }
+
+ try
{
var mensagem = new MqttApplicationMessageBuilder()
.WithTopic(topico)
- .WithPayload(string.Empty) // Mensagem vazia
- .WithRetainFlag(true) // Marca como retained
+ .WithPayload(Array.Empty())
+ .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
+ .WithRetainFlag(true)
.Build();
- await _client.PublishAsync(mensagem);
- _logDebug($"Mensagem retida limpa para o tópico: {topico}");
+ using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(_serviceCts.Token))
+ {
+ timeoutCts.CancelAfter(PublishTimeoutMs);
+ await _client.PublishAsync(mensagem, timeoutCts.Token).ConfigureAwait(false);
+ }
+
+ SafeLog("Mensagem retida limpa para o tópico: " + topico);
+ }
+ catch (Exception ex)
+ {
+ RegisterError("Erro ao limpar retained de '" + topico + "': " + ex.Message);
}
}
- public class MqttTopicosModel
+ // -----------------------------------------------------------------
+ // RECEPÇÃO / DISPATCH
+ // -----------------------------------------------------------------
+
+ private async Task HandleApplicationMessageAsync(MqttApplicationMessageReceivedEventArgs e)
{
+ if (e == null || e.ApplicationMessage == null || IsDisposed || IsStopping)
+ return;
+
+ string topicName = e.ApplicationMessage.Topic ?? string.Empty;
+ byte[] sourcePayload = e.ApplicationMessage.Payload ?? Array.Empty();
+
+ // Cópia defensiva. O callback não depende do ciclo de vida interno do MQTTnet.
+ byte[] payload = sourcePayload.Length == 0
+ ? Array.Empty()
+ : sourcePayload.ToArray();
+
+ Interlocked.Increment(ref _messagesReceived);
+ Interlocked.Add(ref _receivedBytes, payload.LongLength);
+
+ lock (_metricsLock)
+ {
+ _lastReceiveUtc = DateTime.UtcNow;
+ }
+
+ MqttTopicosModel topic = FindSubscribedTopic(topicName);
+
+ if (topic == null || !topic.Habilitado)
+ {
+ Interlocked.Increment(ref _messagesWithoutTopic);
+ return;
+ }
+
+ var mensagem = new MqttTopicosMensagensModel
+ {
+ Momento = DateTime.Now,
+ MomentoUtc = DateTime.UtcNow,
+ RecebidoMonotonicTimestamp = Stopwatch.GetTimestamp(),
+ Cliente = e.ClientId,
+ Topico = topicName,
+ Bytes = payload,
+ Retained = e.ApplicationMessage.Retain,
+ QoS = FromMqttNetQos(e.ApplicationMessage.QualityOfServiceLevel),
+ PayloadType = topic.PayloadType,
+ };
+
+ topic.RegistrarRecebimento(mensagem);
+
+ if (topic.Callback == null)
+ return;
+
+ if (topic.DispatchMode == MqttDispatchMode.Inline)
+ {
+ // Somente para callbacks explicitamente mínimos, como atualizar um timestamp.
+ // O padrão permanece Sequential para não bloquear o loop MQTT.
+ try
+ {
+ await topic.Callback(mensagem).ConfigureAwait(false);
+ topic.RegistrarCallbackSucesso();
+ }
+ catch (Exception ex)
+ {
+ Interlocked.Increment(ref _callbackFailures);
+ topic.RegistrarCallbackFalha(ex);
+ RegisterError("Erro no callback inline do tópico '" + topic.Topico + "': " + ex.Message);
+ }
+
+ return;
+ }
+
+ bool accepted = topic.Enfileirar(mensagem);
+ if (!accepted)
+ {
+ Interlocked.Increment(ref _dispatchDropped);
+ }
+ }
+
+ private MqttTopicosModel FindSubscribedTopic(string actualTopic)
+ {
+ MqttTopicosModel[] snapshot = GetTopicsSnapshot();
+
+ // Primeiro procura correspondência exata, que é o caso mais comum e rápido.
+ MqttTopicosModel exact = snapshot.FirstOrDefault(x =>
+ x != null &&
+ x.Inscrever &&
+ string.Equals(x.Topico, actualTopic, StringComparison.Ordinal));
+
+ if (exact != null)
+ return exact;
+
+ return snapshot.FirstOrDefault(x =>
+ x != null &&
+ x.Inscrever &&
+ TopicMatchesFilter(x.Topico, actualTopic));
+ }
+
+ private static bool TopicMatchesFilter(string filter, string topic)
+ {
+ if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(topic))
+ return false;
+
+ string[] f = filter.Split('/');
+ string[] t = topic.Split('/');
+
+ int i = 0;
+ for (; i < f.Length; i++)
+ {
+ if (f[i] == "#")
+ return i == f.Length - 1;
+
+ if (i >= t.Length)
+ return false;
+
+ if (f[i] == "+")
+ continue;
+
+ if (!string.Equals(f[i], t[i], StringComparison.Ordinal))
+ return false;
+ }
+
+ return i == t.Length;
+ }
+
+ private MqttTopicosModel[] GetTopicsSnapshot()
+ {
+ lock (_topicsLock)
+ {
+ return Topicos
+ .Where(x => x != null)
+ .ToArray();
+ }
+ }
+
+ internal void NotifyCallbackFailure(MqttTopicosModel topic, Exception ex)
+ {
+ Interlocked.Increment(ref _callbackFailures);
+ RegisterError("Erro no callback do tópico '" + topic.Topico + "': " + ex.Message);
+ }
+
+ internal void NotifyDispatchDropped()
+ {
+ Interlocked.Increment(ref _dispatchDropped);
+ }
+
+ // -----------------------------------------------------------------
+ // MÉTRICAS
+ // -----------------------------------------------------------------
+
+ public MqttServiceMetrics GetMetrics()
+ {
+ DateTime lastConnected;
+ DateTime lastDisconnected;
+ DateTime lastPublish;
+ DateTime lastReceive;
+ DateTime lastErrorUtc;
+ string lastError;
+ double lastPublishDuration;
+ double lastConnectDuration;
+
+ lock (_metricsLock)
+ {
+ lastConnected = _lastConnectedUtc;
+ lastDisconnected = _lastDisconnectedUtc;
+ lastPublish = _lastPublishUtc;
+ lastReceive = _lastReceiveUtc;
+ lastErrorUtc = _lastErrorUtc;
+ lastError = _lastError;
+ lastPublishDuration = _lastPublishDurationMs;
+ lastConnectDuration = _lastConnectDurationMs;
+ }
+
+ MqttTopicosModel[] topics = GetTopicsSnapshot();
+
+ return new MqttServiceMetrics
+ {
+ BrokerAddress = _brokerAddr,
+ BrokerPort = _brokerPort,
+ ClientId = _clientId,
+ Local = _local,
+ Started = Volatile.Read(ref _started) == 1,
+ Connected = _client.IsConnected,
+ Stopping = IsStopping,
+ Disposed = IsDisposed,
+ ManualDisconnect = Volatile.Read(ref _manualDisconnect) == 1,
+
+ ConnectionAttempts = Interlocked.Read(ref _connectionAttempts),
+ ConnectionSuccesses = Interlocked.Read(ref _connectionSuccesses),
+ Disconnects = Interlocked.Read(ref _disconnects),
+ ReconnectSchedules = Interlocked.Read(ref _reconnectSchedules),
+ CurrentReconnectBackoffSeconds = GetReconnectBackoff(),
+ NextReconnectInMs = GetNextReconnectInMs(),
+
+ PublishAttempts = Interlocked.Read(ref _publishAttempts),
+ PublishSuccesses = Interlocked.Read(ref _publishSuccesses),
+ PublishFailures = Interlocked.Read(ref _publishFailures),
+ PublishDroppedDisconnected = Interlocked.Read(ref _publishDroppedDisconnected),
+ PublishTimeouts = Interlocked.Read(ref _publishTimeouts),
+ PublishedBytes = Interlocked.Read(ref _publishedBytes),
+
+ MessagesReceived = Interlocked.Read(ref _messagesReceived),
+ ReceivedBytes = Interlocked.Read(ref _receivedBytes),
+ MessagesWithoutTopic = Interlocked.Read(ref _messagesWithoutTopic),
+ CallbackFailures = Interlocked.Read(ref _callbackFailures),
+ DispatchDropped = Interlocked.Read(ref _dispatchDropped),
+
+ LastConnectedUtc = lastConnected,
+ LastDisconnectedUtc = lastDisconnected,
+ LastPublishUtc = lastPublish,
+ LastReceiveUtc = lastReceive,
+ LastErrorUtc = lastErrorUtc,
+ LastError = lastError,
+ LastPublishDurationMs = lastPublishDuration,
+ LastConnectDurationMs = lastConnectDuration,
+
+ Topics = topics.Select(x => x.GetMetrics()).ToList(),
+ ReconnectTimer = _reconnectTimer.GetMetrics(),
+ };
+ }
+
+ private double GetReconnectBackoff()
+ {
+ lock (_stateLock)
+ {
+ return _reconnectBackoffSeconds;
+ }
+ }
+
+ private double GetNextReconnectInMs()
+ {
+ long next = Interlocked.Read(ref _nextReconnectTimestamp);
+ if (next <= 0)
+ return 0.0;
+
+ long remaining = next - Stopwatch.GetTimestamp();
+ return Math.Max(0.0, StopwatchTicksToMilliseconds(remaining));
+ }
+
+ private void RegisterError(string message)
+ {
+ lock (_metricsLock)
+ {
+ _lastError = message;
+ _lastErrorUtc = DateTime.UtcNow;
+ }
+
+ SafeLog(message);
+ }
+
+ private void SafeLog(string message)
+ {
+ try
+ {
+ _logDebug("[" + _clientId + "] " + message);
+ }
+ catch
+ {
+ // Logging nunca deve comprometer o transporte.
+ }
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (IsDisposed)
+ throw new ObjectDisposedException(nameof(MqttService));
+ }
+
+ private static string SanitizeTimerId(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return "mqtt";
+
+ var sb = new StringBuilder(value.Length);
+ foreach (char c in value)
+ {
+ if (char.IsLetterOrDigit(c) || c == '-' || c == '_')
+ sb.Append(c);
+ else
+ sb.Append('_');
+ }
+
+ return sb.ToString();
+ }
+
+ private static long MillisecondsToStopwatchTicks(double milliseconds)
+ {
+ return (long)Math.Ceiling(milliseconds * Stopwatch.Frequency / 1000.0);
+ }
+
+ private static double StopwatchTicksToMilliseconds(long ticks)
+ {
+ return ticks * 1000.0 / Stopwatch.Frequency;
+ }
+
+ private static double ElapsedMilliseconds(long startedTimestamp)
+ {
+ return StopwatchTicksToMilliseconds(Stopwatch.GetTimestamp() - startedTimestamp);
+ }
+
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) == 1)
+ return;
+
+ try
+ {
+ GetOrCreateStopTask(removeFromRegistry: true).GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ SafeLog("Erro no Dispose MQTT: " + ex.Message);
+ }
+
+ try { _reconnectTimer.Dispose(); } catch { }
+ try { _client.Dispose(); } catch { }
+ try { _serviceCts.Dispose(); } catch { }
+ try { _connectLock.Dispose(); } catch { }
+ try { _publishLock.Dispose(); } catch { }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) == 1)
+ return;
+
+ try
+ {
+ await GetOrCreateStopTask(removeFromRegistry: true).ConfigureAwait(false);
+ }
+ finally
+ {
+ try { _reconnectTimer.Dispose(); } catch { }
+ try { _client.Dispose(); } catch { }
+ try { _serviceCts.Dispose(); } catch { }
+ try { _connectLock.Dispose(); } catch { }
+ try { _publishLock.Dispose(); } catch { }
+ }
+ }
+
+ // =================================================================
+ // MODELOS DE TÓPICO E MENSAGEM
+ // =================================================================
+
+ public sealed class MqttTopicosModel
+ {
+ private readonly object _historyLock = new object();
+ private readonly object _queueLock = new object();
+ private readonly object _metricsLock = new object();
+ private readonly SemaphoreSlim _queueSignal = new SemaphoreSlim(0, int.MaxValue);
+ private readonly SemaphoreSlim _subscribeLock = new SemaphoreSlim(1, 1);
+ private readonly CancellationTokenSource _workerCts;
+ private readonly ConcurrentQueue _queue =
+ new ConcurrentQueue();
+
+ private readonly MqttService _owner;
+ private Task _workerTask = Task.CompletedTask;
+ private MqttTopicosMensagensModel _latestPending;
+
+ private int _workerStarted;
+ private int _pendingCount;
+ private int _subscribed;
+ private int _subscribeLockHeld;
+
+ private long _messagesReceived;
+ private long _receivedBytes;
+ private long _messagesQueued;
+ private long _messagesProcessed;
+ private long _messagesDropped;
+ private long _messagesReplaced;
+ private long _callbackFailures;
+ private long _publishedMessages;
+ private long _publishedBytes;
+ private long _publishFailures;
+
+ private DateTime _lastMessageUtc = DateTime.MinValue;
+ private DateTime _lastProcessedUtc = DateTime.MinValue;
+ private DateTime _lastPublishUtc = DateTime.MinValue;
+ private string _lastError;
+ private double _lastCallbackDurationMs;
+ private double _lastPublishDurationMs;
+
+ internal MqttTopicosModel(MqttService owner, CancellationToken serviceToken)
+ {
+ _owner = owner;
+ _workerCts = CancellationTokenSource.CreateLinkedTokenSource(serviceToken);
+ Mensagens = new List();
+ }
+
+ // Construtor público mantido para compatibilidade com eventuais criações externas.
+ public MqttTopicosModel()
+ {
+ _workerCts = new CancellationTokenSource();
+ Mensagens = new List();
+ }
+
public string Topico { get; set; }
public int MensagensManter { get; set; } = 30;
public List Mensagens { get; set; }
public bool Inscrever { get; set; }
public Func Callback { get; set; }
+
+ public bool Habilitado { get; set; } = true;
+ public MqttPayloadType PayloadType { get; set; } = MqttPayloadType.Auto;
+ public MqttDispatchMode DispatchMode { get; set; } = MqttDispatchMode.Sequential;
+ public int QueueCapacity { get; set; } = 64;
+ public MqttQueueOverflowPolicy OverflowPolicy { get; set; } = MqttQueueOverflowPolicy.DropOldest;
+ public MqttQosLevel QoS { get; set; } = MqttQosLevel.AtMostOnce;
+ public bool Retain { get; set; }
+ public bool Inscrito { get { return Volatile.Read(ref _subscribed) == 1; } }
+
+ internal void AtualizarConfiguracao(
+ bool inscrever,
+ int mensagensManter,
+ Func callback,
+ MqttPayloadType payloadType,
+ MqttDispatchMode dispatchMode,
+ int queueCapacity,
+ MqttQueueOverflowPolicy overflowPolicy,
+ MqttQosLevel qos,
+ bool retain)
+ {
+ Inscrever = inscrever;
+ MensagensManter = mensagensManter;
+ Callback = callback;
+ PayloadType = payloadType;
+ DispatchMode = dispatchMode;
+ QueueCapacity = queueCapacity;
+ OverflowPolicy = overflowPolicy;
+ QoS = qos;
+ Retain = retain;
+ Habilitado = true;
+ }
+
+ internal void GarantirWorkerIniciado()
+ {
+ if (Callback == null || DispatchMode == MqttDispatchMode.Inline)
+ return;
+
+ if (Interlocked.CompareExchange(ref _workerStarted, 1, 0) != 0)
+ return;
+
+ _workerTask = Task.Run(WorkerLoopAsync);
+ }
+
+ internal void RegistrarRecebimento(MqttTopicosMensagensModel message)
+ {
+ Interlocked.Increment(ref _messagesReceived);
+ Interlocked.Add(ref _receivedBytes, message?.Bytes?.LongLength ?? 0L);
+
+ lock (_metricsLock)
+ {
+ _lastMessageUtc = DateTime.UtcNow;
+ }
+
+ if (MensagensManter <= 0)
+ return;
+
+ lock (_historyLock)
+ {
+ if (Mensagens == null)
+ Mensagens = new List();
+
+ Mensagens.Add(message);
+
+ while (Mensagens.Count > MensagensManter)
+ Mensagens.RemoveAt(0);
+ }
+ }
+
+ internal bool Enfileirar(MqttTopicosMensagensModel message)
+ {
+ if (!Habilitado || Callback == null)
+ return false;
+
+ GarantirWorkerIniciado();
+
+ if (DispatchMode == MqttDispatchMode.LatestOnly)
+ {
+ lock (_queueLock)
+ {
+ if (_latestPending != null)
+ {
+ Interlocked.Increment(ref _messagesReplaced);
+ Interlocked.Increment(ref _messagesDropped);
+ _owner?.NotifyDispatchDropped();
+ }
+
+ _latestPending = message;
+
+ if (Interlocked.Exchange(ref _pendingCount, 1) == 0)
+ _queueSignal.Release();
+ }
+
+ Interlocked.Increment(ref _messagesQueued);
+ return true;
+ }
+
+ int capacity = Math.Max(1, QueueCapacity);
+ int pending = Volatile.Read(ref _pendingCount);
+
+ if (pending >= capacity)
+ {
+ if (OverflowPolicy == MqttQueueOverflowPolicy.DropNewest)
+ {
+ Interlocked.Increment(ref _messagesDropped);
+ _owner?.NotifyDispatchDropped();
+ return false;
+ }
+
+ MqttTopicosMensagensModel dropped;
+ if (_queue.TryDequeue(out dropped))
+ {
+ Interlocked.Decrement(ref _pendingCount);
+ Interlocked.Increment(ref _messagesDropped);
+ _owner?.NotifyDispatchDropped();
+ }
+ }
+
+ _queue.Enqueue(message);
+ Interlocked.Increment(ref _pendingCount);
+ Interlocked.Increment(ref _messagesQueued);
+ _queueSignal.Release();
+ return true;
+ }
+
+ private async Task WorkerLoopAsync()
+ {
+ CancellationToken token = _workerCts.Token;
+
+ try
+ {
+ while (!token.IsCancellationRequested)
+ {
+ await _queueSignal.WaitAsync(token).ConfigureAwait(false);
+
+ MqttTopicosMensagensModel message = null;
+
+ if (DispatchMode == MqttDispatchMode.LatestOnly)
+ {
+ lock (_queueLock)
+ {
+ message = _latestPending;
+ _latestPending = null;
+ Interlocked.Exchange(ref _pendingCount, 0);
+ }
+ }
+ else
+ {
+ if (_queue.TryDequeue(out message))
+ Interlocked.Decrement(ref _pendingCount);
+ }
+
+ if (message == null || Callback == null || !Habilitado)
+ continue;
+
+ long started = Stopwatch.GetTimestamp();
+
+ try
+ {
+ await Callback(message).ConfigureAwait(false);
+ Interlocked.Increment(ref _messagesProcessed);
+
+ lock (_metricsLock)
+ {
+ _lastProcessedUtc = DateTime.UtcNow;
+ _lastCallbackDurationMs = ElapsedMilliseconds(started);
+ }
+ }
+ catch (OperationCanceledException) when (token.IsCancellationRequested)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ RegistrarCallbackFalha(ex);
+ _owner?.NotifyCallbackFailure(this, ex);
+ }
+ }
+ }
+ catch (OperationCanceledException) when (token.IsCancellationRequested)
+ {
+ }
+ catch (Exception ex)
+ {
+ RegistrarCallbackFalha(ex);
+ _owner?.NotifyCallbackFailure(this, ex);
+ }
+ }
+
+ internal void RegistrarCallbackSucesso()
+ {
+ Interlocked.Increment(ref _messagesProcessed);
+ lock (_metricsLock)
+ {
+ _lastProcessedUtc = DateTime.UtcNow;
+ }
+ }
+
+ internal void RegistrarCallbackFalha(Exception ex)
+ {
+ Interlocked.Increment(ref _callbackFailures);
+ lock (_metricsLock)
+ {
+ _lastError = ex?.ToString();
+ }
+ }
+
+ internal void RegistrarPublicacao(long bytes, double durationMs, bool success, string error)
+ {
+ if (success)
+ {
+ Interlocked.Increment(ref _publishedMessages);
+ Interlocked.Add(ref _publishedBytes, bytes);
+ }
+ else
+ {
+ Interlocked.Increment(ref _publishFailures);
+ }
+
+ lock (_metricsLock)
+ {
+ _lastPublishUtc = DateTime.UtcNow;
+ _lastPublishDurationMs = durationMs;
+ if (!success)
+ _lastError = error;
+ }
+ }
+
+ internal async Task EntrarInscricaoAsync(CancellationToken token)
+ {
+ await _subscribeLock.WaitAsync(token).ConfigureAwait(false);
+ Interlocked.Exchange(ref _subscribeLockHeld, 1);
+ return true;
+ }
+
+ internal void SairInscricao()
+ {
+ if (Interlocked.Exchange(ref _subscribeLockHeld, 0) == 1)
+ _subscribeLock.Release();
+ }
+
+ internal void MarcarInscrito()
+ {
+ Volatile.Write(ref _subscribed, 1);
+ }
+
+ internal void MarcarNaoInscrito()
+ {
+ Volatile.Write(ref _subscribed, 0);
+ }
+
+ internal async Task PararWorkerAsync()
+ {
+ try { _workerCts.Cancel(); } catch { }
+
+ try
+ {
+ if (_workerTask != null)
+ await _workerTask.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch
+ {
+ }
+ }
+
+ public MqttTopicMetrics GetMetrics()
+ {
+ DateTime lastMessage;
+ DateTime lastProcessed;
+ DateTime lastPublish;
+ string lastError;
+ double callbackMs;
+ double publishMs;
+
+ lock (_metricsLock)
+ {
+ lastMessage = _lastMessageUtc;
+ lastProcessed = _lastProcessedUtc;
+ lastPublish = _lastPublishUtc;
+ lastError = _lastError;
+ callbackMs = _lastCallbackDurationMs;
+ publishMs = _lastPublishDurationMs;
+ }
+
+ return new MqttTopicMetrics
+ {
+ Topic = Topico,
+ Subscribe = Inscrever,
+ Subscribed = Inscrito,
+ Enabled = Habilitado,
+ PayloadType = PayloadType,
+ DispatchMode = DispatchMode,
+ QueueCapacity = QueueCapacity,
+ QueueDepth = Volatile.Read(ref _pendingCount),
+ OverflowPolicy = OverflowPolicy,
+ QoS = QoS,
+ Retain = Retain,
+
+ MessagesReceived = Interlocked.Read(ref _messagesReceived),
+ ReceivedBytes = Interlocked.Read(ref _receivedBytes),
+ MessagesQueued = Interlocked.Read(ref _messagesQueued),
+ MessagesProcessed = Interlocked.Read(ref _messagesProcessed),
+ MessagesDropped = Interlocked.Read(ref _messagesDropped),
+ MessagesReplaced = Interlocked.Read(ref _messagesReplaced),
+ CallbackFailures = Interlocked.Read(ref _callbackFailures),
+
+ PublishedMessages = Interlocked.Read(ref _publishedMessages),
+ PublishedBytes = Interlocked.Read(ref _publishedBytes),
+ PublishFailures = Interlocked.Read(ref _publishFailures),
+
+ LastMessageUtc = lastMessage,
+ LastProcessedUtc = lastProcessed,
+ LastPublishUtc = lastPublish,
+ LastCallbackDurationMs = callbackMs,
+ LastPublishDurationMs = publishMs,
+ LastError = lastError,
+ };
+ }
}
- public class MqttTopicosMensagensModel
+ public sealed class MqttTopicosMensagensModel
{
+ private string _messageText;
+ private bool _messageDecoded;
+ private byte[] _bytes = Array.Empty();
+
public DateTime Momento { get; set; }
+ public DateTime MomentoUtc { get; set; }
+ public long RecebidoMonotonicTimestamp { get; set; }
public string Cliente { get; set; }
- public string Mensagem { get; set; }
- public byte[] Bytes { get; set; }
+ public string Topico { get; set; }
+ public bool Retained { get; set; }
+ public MqttQosLevel QoS { get; set; }
+ public MqttPayloadType PayloadType { get; set; }
+
+ ///
+ /// Conversão UTF-8 preguiçosa. RTCM e outros payloads binários não geram string
+ /// a menos que algum consumidor realmente leia esta propriedade.
+ ///
+ public string Mensagem
+ {
+ get
+ {
+ if (_messageDecoded)
+ return _messageText;
+
+ if (PayloadType == MqttPayloadType.Binary)
+ return string.Empty;
+
+ try
+ {
+ _messageText = Encoding.UTF8.GetString(_bytes ?? Array.Empty());
+ }
+ catch
+ {
+ _messageText = string.Empty;
+ }
+
+ _messageDecoded = true;
+ return _messageText;
+ }
+ set
+ {
+ _messageText = value ?? string.Empty;
+ _messageDecoded = true;
+ }
+ }
+
+ public byte[] Bytes
+ {
+ get { return _bytes; }
+ set
+ {
+ _bytes = value ?? Array.Empty();
+ if (!_messageDecoded)
+ _messageText = null;
+ }
+ }
+
+ public double IdadeRecepcaoMs
+ {
+ get
+ {
+ if (RecebidoMonotonicTimestamp <= 0)
+ return -1.0;
+
+ return StopwatchTicksToMilliseconds(
+ Stopwatch.GetTimestamp() - RecebidoMonotonicTimestamp);
+ }
+ }
}
-}
\ No newline at end of file
+}
+
+public enum MqttQosLevel
+{
+ AtMostOnce = 0,
+ AtLeastOnce = 1,
+ ExactlyOnce = 2,
+}
+
+public enum MqttPayloadType
+{
+ Auto = 0,
+ Text = 1,
+ Binary = 2,
+}
+
+public enum MqttDispatchMode
+{
+ ///
+ /// Callback executado no handler MQTT. Usar apenas para operações mínimas,
+ /// como atualizar um timestamp monotônico.
+ ///
+ Inline = 0,
+
+ ///
+ /// Worker dedicado por tópico, preservando ordem e limitando backlog.
+ ///
+ Sequential = 1,
+
+ ///
+ /// Mantém somente a mensagem pendente mais recente.
+ /// Ideal para posição, telemetria e estados substituíveis.
+ ///
+ LatestOnly = 2,
+}
+
+public enum MqttQueueOverflowPolicy
+{
+ DropOldest = 0,
+ DropNewest = 1,
+}
+
+public enum MqttPublishStatus
+{
+ Success = 0,
+ InvalidTopic = 1,
+ TopicDisabled = 2,
+ PublishNotAllowed = 3,
+ Disconnected = 4,
+ ServiceStopping = 5,
+ Timeout = 6,
+ Canceled = 7,
+ Error = 8,
+}
+
+public sealed class MqttPublishResult
+{
+ public bool Succeeded { get; private set; }
+ public MqttPublishStatus Status { get; private set; }
+ public string Error { get; private set; }
+ public int PayloadBytes { get; private set; }
+ public double DurationMs { get; private set; }
+
+ public static MqttPublishResult Success(int payloadBytes, double durationMs)
+ {
+ return new MqttPublishResult
+ {
+ Succeeded = true,
+ Status = MqttPublishStatus.Success,
+ PayloadBytes = payloadBytes,
+ DurationMs = durationMs,
+ };
+ }
+
+ public static MqttPublishResult Failure(
+ MqttPublishStatus status,
+ string error,
+ int payloadBytes = 0,
+ double durationMs = 0.0)
+ {
+ return new MqttPublishResult
+ {
+ Succeeded = false,
+ Status = status,
+ Error = error,
+ PayloadBytes = payloadBytes,
+ DurationMs = durationMs,
+ };
+ }
+}
+
+public sealed class MqttServiceMetrics
+{
+ public string BrokerAddress { get; set; }
+ public int BrokerPort { get; set; }
+ public string ClientId { get; set; }
+ public bool Local { get; set; }
+ public bool Started { get; set; }
+ public bool Connected { get; set; }
+ public bool Stopping { get; set; }
+ public bool Disposed { get; set; }
+ public bool ManualDisconnect { get; set; }
+
+ public long ConnectionAttempts { get; set; }
+ public long ConnectionSuccesses { get; set; }
+ public long Disconnects { get; set; }
+ public long ReconnectSchedules { get; set; }
+ public double CurrentReconnectBackoffSeconds { get; set; }
+ public double NextReconnectInMs { get; set; }
+
+ public long PublishAttempts { get; set; }
+ public long PublishSuccesses { get; set; }
+ public long PublishFailures { get; set; }
+ public long PublishDroppedDisconnected { get; set; }
+ public long PublishTimeouts { get; set; }
+ public long PublishedBytes { get; set; }
+
+ public long MessagesReceived { get; set; }
+ public long ReceivedBytes { get; set; }
+ public long MessagesWithoutTopic { get; set; }
+ public long CallbackFailures { get; set; }
+ public long DispatchDropped { get; set; }
+
+ public DateTime LastConnectedUtc { get; set; }
+ public DateTime LastDisconnectedUtc { get; set; }
+ public DateTime LastPublishUtc { get; set; }
+ public DateTime LastReceiveUtc { get; set; }
+ public DateTime LastErrorUtc { get; set; }
+ public string LastError { get; set; }
+ public double LastPublishDurationMs { get; set; }
+ public double LastConnectDurationMs { get; set; }
+
+ public List Topics { get; set; }
+ public AsyncTaskTimerMetrics ReconnectTimer { get; set; }
+}
+
+public sealed class MqttTopicMetrics
+{
+ public string Topic { get; set; }
+ public bool Subscribe { get; set; }
+ public bool Subscribed { get; set; }
+ public bool Enabled { get; set; }
+ public MqttPayloadType PayloadType { get; set; }
+ public MqttDispatchMode DispatchMode { get; set; }
+ public int QueueCapacity { get; set; }
+ public int QueueDepth { get; set; }
+ public MqttQueueOverflowPolicy OverflowPolicy { get; set; }
+ public MqttQosLevel QoS { get; set; }
+ public bool Retain { get; set; }
+
+ public long MessagesReceived { get; set; }
+ public long ReceivedBytes { get; set; }
+ public long MessagesQueued { get; set; }
+ public long MessagesProcessed { get; set; }
+ public long MessagesDropped { get; set; }
+ public long MessagesReplaced { get; set; }
+ public long CallbackFailures { get; set; }
+
+ public long PublishedMessages { get; set; }
+ public long PublishedBytes { get; set; }
+ public long PublishFailures { get; set; }
+
+ public DateTime LastMessageUtc { get; set; }
+ public DateTime LastProcessedUtc { get; set; }
+ public DateTime LastPublishUtc { get; set; }
+ public double LastCallbackDurationMs { get; set; }
+ public double LastPublishDurationMs { get; set; }
+ public string LastError { get; set; }
+}
diff --git a/AgroBase/AgroBase/Services/SerialService.cs b/AgroBase/AgroBase/Services/SerialService.cs
index 27387fb61..9e99b5dc5 100644
--- a/AgroBase/AgroBase/Services/SerialService.cs
+++ b/AgroBase/AgroBase/Services/SerialService.cs
@@ -1,8 +1,10 @@
-using AgroBase.Forms;
+using AgroBase.Forms;
using AgroBase.Models;
using AgroBase.Services.Operadores;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
@@ -14,8 +16,25 @@ using static AgroBase.Models.Enums;
namespace AgroBase.Services
{
+ ///
+ /// Descoberta e supervisão das conexões físicas do rover.
+ ///
+ /// Garantias:
+ /// - somente uma varredura executa por vez;
+ /// - shutdown cancela e aguarda a varredura atual;
+ /// - leituras com timeout não abandonam threads bloqueadas;
+ /// - escritas na mesma porta são serializadas;
+ /// - uma porta que não é CAN não remove dispositivos CAN válidos;
+ /// - rotinas de versionamento/sincronização não se acumulam;
+ /// - tempos internos usam Stopwatch;
+ /// - operações de interface nunca bloqueiam a varredura.
+ ///
public class SerialService
{
+ // ============================================================
+ // CONTRATO PÚBLICO LEGADO
+ // ============================================================
+
public static int InteraloVerificacao = 5000;
public static readonly string BeginLine = "@";
@@ -26,436 +45,968 @@ namespace AgroBase.Services
public static readonly string SplitParams = ",";
public static readonly string SplitConfig = "*";
public static readonly string SplitSubParams = "~";
- public static List DispositivosMotores = new List() { T_Code.Mov, T_Code.Dir };
- public static List DispositivosConexaoInicial = new List() { T_Code.Mvd, T_Code.Atu, T_Code.Sen };
- public static List DispositivosProprios = new List() { T_Code.Atu, T_Code.Sen };
- public static List DispositivosModbus = new List() { T_Code.Pzm, T_Code.Wit, T_Code.A05 };
- public static List DispositivosCan = new List() { T_Code.Mov, T_Code.Oid, T_Code.Dir, T_Code.Mks, T_Code.Sen, T_Code.Atu };
- public static List DispositivosMultiplos = new List() { T_Code.Bld, T_Code.Mks, T_Code.Mvd, T_Code.Oid, T_Code.Mov, T_Code.Dir };
- public static List DispositivosMapeados = new List()
+ public static readonly List DispositivosMotores =
+ new List { T_Code.Mov, T_Code.Dir };
+
+ public static readonly List DispositivosConexaoInicial =
+ new List { T_Code.Mvd, T_Code.Atu, T_Code.Sen };
+
+ public static readonly List DispositivosProprios =
+ new List { T_Code.Atu, T_Code.Sen };
+
+ public static readonly List DispositivosModbus =
+ new List { T_Code.Pzm, T_Code.Wit, T_Code.A05 };
+
+ public static readonly List DispositivosCan =
+ new List
+ {
+ T_Code.Mov,
+ T_Code.Oid,
+ T_Code.Dir,
+ T_Code.Mks,
+ T_Code.Sen,
+ T_Code.Atu
+ };
+
+ public static readonly List DispositivosMultiplos =
+ new List
+ {
+ T_Code.Bld,
+ T_Code.Mks,
+ T_Code.Mvd,
+ T_Code.Oid,
+ T_Code.Mov,
+ T_Code.Dir
+ };
+
+ ///
+ /// Mantido público por compatibilidade.
+ /// Código novo deve preferir GetDispositivosMapeadosSnapshot(),
+ /// AdicionarOuAtualizarDispositivoMapeado() e RemoverDispositivoMapeado().
+ ///
+ public static List DispositivosMapeados =
+ CriarListaInicialDispositivos();
+
+ // ============================================================
+ // SINCRONIZAÇÃO E CICLO DE VIDA
+ // ============================================================
+
+ private static readonly SemaphoreSlim _scanGate =
+ new SemaphoreSlim(1, 1);
+
+ private static readonly SemaphoreSlim _updateDevicesGate =
+ new SemaphoreSlim(1, 1);
+
+ private static readonly SemaphoreSlim _versioningGate =
+ new SemaphoreSlim(1, 1);
+
+ private static readonly SemaphoreSlim _syncDataGate =
+ new SemaphoreSlim(1, 1);
+
+ private static readonly object _mappedDevicesLock =
+ new object();
+
+ private static readonly object _connectedDevicesLock =
+ new object();
+
+ private static readonly object _lifecycleLock =
+ new object();
+
+ private static readonly object _metricsTextLock =
+ new object();
+
+ private static readonly ConcurrentDictionary<
+ string,
+ SemaphoreSlim> _portIoLocks =
+ new ConcurrentDictionary<
+ string,
+ SemaphoreSlim>(
+ StringComparer.OrdinalIgnoreCase
+ );
+
+ private static readonly ConcurrentDictionary<
+ string,
+ int> _missingPortConfirmations =
+ new ConcurrentDictionary<
+ string,
+ int>(
+ StringComparer.OrdinalIgnoreCase
+ );
+
+ public static int ConfirmacoesPortaAusente { get; set; } = 2;
+
+ private static CancellationTokenSource _lifetimeCts =
+ new CancellationTokenSource();
+
+ private static CancellationTokenSource _currentScanCts;
+
+ private static long _scanGeneration;
+ private static int _isScanning;
+ private static int _isClosing;
+
+ // ============================================================
+ // MANUTENÇÃO
+ // ============================================================
+
+ public static int IntervaloVersionamentoMs { get; set; } = 60000;
+ public static int IntervaloSincronizacaoMs { get; set; } = 60000;
+
+ private static long _lastVersioningStartMono;
+ private static long _lastSyncStartMono;
+
+ // ============================================================
+ // MÉTRICAS
+ // ============================================================
+
+ private static long _scanStarted;
+ private static long _scanCompleted;
+ private static long _scanSkipped;
+ private static long _scanCanceled;
+ private static long _scanErrors;
+
+ private static long _portsEnumerated;
+ private static long _portsProbed;
+ private static long _portsBusy;
+ private static long _portsProbeErrors;
+
+ private static long _gpsFound;
+ private static long _loraFound;
+ private static long _canAdapterFound;
+ private static long _devicesRemoved;
+
+ private static long _serialWriteAttempts;
+ private static long _serialWriteSuccesses;
+ private static long _serialWriteTimeouts;
+ private static long _serialWriteErrors;
+
+ private static long _lastScanStartMono;
+ private static long _lastScanEndMono;
+ private static double _lastScanDurationMs;
+ private static double _maxScanDurationMs;
+
+ private static long _versioningRuns;
+ private static long _versioningErrors;
+ private static long _syncRuns;
+ private static long _syncErrors;
+
+ private static string _lastError;
+
+ private static int _uiUpdateScheduled;
+ private static string _pendingUiMessage;
+
+ // ============================================================
+ // ESCRITA SERIAL
+ // ============================================================
+
+ public static bool EnviarDadosPortaSerial(
+ SerialPort porta,
+ string dados,
+ bool DebugMode = false,
+ int timeout = 2000)
{
- new DispositivoDetalhesModel()
+ if (string.IsNullOrEmpty(dados))
{
- Dispositivo = T_Code.Npc,
- Endereco = EthernetService.ObterIpAtual(VariaveisEquipamento.Parametros.comunicacao_interface),
- Versao = Variaveis.Versao,
- Mod_ID = ""
- }
- };
- private static bool VarreduraEmAndamento = false;
+ if (DebugMode)
+ Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] Nenhum dado para enviar.");
-
- public static bool EnviarDadosPortaSerial(SerialPort _porta, string Dados, bool DebugMode = false, int timeout = 2000)
- {
- bool ForceDebug = false;
- if (ForceDebug)
- {
- DebugMode = true;
- }
-
- // Verifica se a porta está definida
- if (_porta == null)
- {
- Console.WriteLine("A porta serial não está definida.");
return false;
}
- // Verifica se a porta está aberta
- if (!_porta.IsOpen)
+ byte[] bytes = Encoding.ASCII.GetBytes(dados);
+
+ return EnviarDadosPortaSerial(
+ porta,
+ bytes,
+ 0,
+ bytes.Length,
+ DebugMode,
+ timeout
+ );
+ }
+
+ public static bool EnviarDadosPortaSerial(
+ SerialPort porta,
+ byte[] dados,
+ int inicio,
+ int comprimento,
+ bool DebugMode = false)
+ {
+ return EnviarDadosPortaSerial(
+ porta,
+ dados,
+ inicio,
+ comprimento,
+ DebugMode,
+ 2000
+ );
+ }
+
+ private static bool EnviarDadosPortaSerial(
+ SerialPort porta,
+ byte[] dados,
+ int inicio,
+ int comprimento,
+ bool debugMode,
+ int timeout)
+ {
+ Interlocked.Increment(ref _serialWriteAttempts);
+
+ if (!ValidarFaixaBuffer(
+ dados,
+ inicio,
+ comprimento))
{
- Console.WriteLine("A porta serial não está aberta.");
+ if (debugMode)
+ Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] Buffer serial inválido.");
+
+ Interlocked.Increment(ref _serialWriteErrors);
return false;
}
+ if (porta == null)
+ {
+ if (debugMode)
+ Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] A porta serial não está definida.");
+
+ Interlocked.Increment(ref _serialWriteErrors);
+ return false;
+ }
+
+ SemaphoreSlim gate = GetPortIoLock(porta);
+
+ bool acquired = false;
+
try
{
- // Define o tempo de espera máximo para a operação de escrita
- _porta.WriteTimeout = timeout; // 2 segundos de timeout para escrita (ajuste conforme necessário)
+ acquired = gate.Wait(
+ Math.Max(1, timeout)
+ );
- // Verifica se há dados para enviar
- if (string.IsNullOrEmpty(Dados))
+ if (!acquired)
{
- if (DebugMode)
- Console.WriteLine("Nenhum dado para enviar.");
+ Interlocked.Increment(
+ ref _serialWriteTimeouts
+ );
+
+ if (debugMode)
+ {
+ Variaveis.MostrarLog(
+ "[SerialService.EnviarDadosPortaSerial] Timeout aguardando acesso exclusivo à porta."
+ );
+ }
+
return false;
}
- // Tenta enviar os dados na porta serial
- _porta.Write(Dados);
- if (DebugMode)
- Console.WriteLine("Dados enviados com sucesso.");
+ if (!porta.IsOpen)
+ {
+ if (debugMode)
+ Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] A porta serial não está aberta.");
+
+ Interlocked.Increment(ref _serialWriteErrors);
+ return false;
+ }
+
+ int oldTimeout = porta.WriteTimeout;
+
+ try
+ {
+ porta.WriteTimeout = Math.Max(1, timeout);
+ porta.Write(dados, inicio, comprimento);
+ }
+ finally
+ {
+ try { porta.WriteTimeout = oldTimeout; }
+ catch { }
+ }
+
+ Interlocked.Increment(
+ ref _serialWriteSuccesses
+ );
+
+ if (debugMode)
+ Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] Dados enviados com sucesso.");
+
return true;
}
- catch (TimeoutException)
+ catch (TimeoutException ex)
{
- // Timeout ocorreu, o que significa que não foi possível escrever os dados no tempo estipulado
- if (DebugMode)
- Console.WriteLine("Erro: Timeout ao tentar enviar dados.");
+ Interlocked.Increment(
+ ref _serialWriteTimeouts
+ );
+
+ RecordError(
+ "[SerialService.Write] Timeout em " +
+ SafePortName(porta) + ": " + ex.Message
+ );
+
return false;
}
catch (InvalidOperationException ex)
{
- // A porta não estava aberta ou algo relacionado a uma falha de operação na porta
- if (DebugMode)
- Console.WriteLine("Erro: A porta serial não está aberta ou houve uma falha na operação. " + ex.Message);
+ Interlocked.Increment(ref _serialWriteErrors);
+
+ RecordError(
+ "[SerialService.Write] Porta indisponível " +
+ SafePortName(porta) + ": " + ex.Message
+ );
+
return false;
}
catch (UnauthorizedAccessException ex)
{
- // Caso a porta serial esteja sendo acessada por outro processo
- if (DebugMode)
- Console.WriteLine("Erro: Acesso negado à porta serial. " + ex.Message);
+ Interlocked.Increment(ref _serialWriteErrors);
+
+ RecordError(
+ "[SerialService.Write] Acesso negado em " +
+ SafePortName(porta) + ": " + ex.Message
+ );
+
+ return false;
+ }
+ catch (IOException ex)
+ {
+ Interlocked.Increment(ref _serialWriteErrors);
+
+ RecordError(
+ "[SerialService.Write] Erro de I/O em " +
+ SafePortName(porta) + ": " + ex.Message
+ );
+
return false;
}
catch (Exception ex)
{
- // Captura outros tipos de erro inesperados
- if (DebugMode)
- Console.WriteLine("Erro inesperado: " + ex.Message);
+ Interlocked.Increment(ref _serialWriteErrors);
+
+ RecordError(
+ "[SerialService.Write] Erro inesperado em " +
+ SafePortName(porta) + ": " + ex.Message
+ );
+
return false;
}
+ finally
+ {
+ if (acquired)
+ gate.Release();
+ }
}
- public static bool EnviarDadosPortaSerial(SerialPort _porta, byte[] Dados, int inicio, int comprimento, bool DebugMode = false)
+ public static async Task EnviarDadosPortaSerialAsync(
+ SerialPort porta,
+ byte[] dados,
+ int inicio,
+ int comprimento,
+ int timeout = 2000,
+ CancellationToken cancellationToken =
+ default(CancellationToken))
{
- bool ForceDebug = false;
- if (ForceDebug)
- {
- DebugMode = true;
- }
+ Interlocked.Increment(ref _serialWriteAttempts);
- // Verifica se a porta está definida
- if (_porta == null)
+ if (!ValidarFaixaBuffer(
+ dados,
+ inicio,
+ comprimento) ||
+ porta == null)
{
- Console.WriteLine("A porta serial não está definida.");
+ Interlocked.Increment(ref _serialWriteErrors);
return false;
}
- // Verifica se a porta está aberta
- if (!_porta.IsOpen)
- {
- Console.WriteLine("A porta serial não está aberta.");
- return false;
- }
+ SemaphoreSlim gate = GetPortIoLock(porta);
- try
+ using (var timeoutCts =
+ CancellationTokenSource.CreateLinkedTokenSource(
+ cancellationToken))
{
- // Define o tempo de espera máximo para a operação de escrita
- _porta.WriteTimeout = 2000; // 2 segundos de timeout para escrita (ajuste conforme necessário)
+ timeoutCts.CancelAfter(
+ Math.Max(1, timeout)
+ );
- // Verifica se há dados para enviar
- if (Dados == null || Dados.Length == 0)
+ bool acquired = false;
+
+ try
{
- if (DebugMode)
- Console.WriteLine("Nenhum dado para enviar.");
+ await gate.WaitAsync(timeoutCts.Token)
+ .ConfigureAwait(false);
+
+ acquired = true;
+
+ if (!porta.IsOpen)
+ {
+ Interlocked.Increment(
+ ref _serialWriteErrors
+ );
+
+ return false;
+ }
+
+ /*
+ * SerialPort.Write é síncrono. O lock e o WriteTimeout
+ * impedem sobreposição e limitam o bloqueio no driver.
+ * Não usamos Task.Run, pois ele abandonaria uma thread
+ * caso o driver ignorasse cancelamento.
+ */
+ int oldTimeout = porta.WriteTimeout;
+
+ try
+ {
+ porta.WriteTimeout =
+ Math.Max(1, timeout);
+
+ porta.Write(
+ dados,
+ inicio,
+ comprimento
+ );
+ }
+ finally
+ {
+ try { porta.WriteTimeout = oldTimeout; }
+ catch { }
+ }
+
+ Interlocked.Increment(
+ ref _serialWriteSuccesses
+ );
+
+ return true;
+ }
+ catch (OperationCanceledException)
+ {
+ Interlocked.Increment(
+ ref _serialWriteTimeouts
+ );
+
return false;
}
+ catch (TimeoutException)
+ {
+ Interlocked.Increment(
+ ref _serialWriteTimeouts
+ );
- // Tenta enviar os dados na porta serial
- _porta.Write(Dados, inicio, comprimento);
- if (DebugMode)
- Console.WriteLine("Dados enviados com sucesso.");
- return true;
- }
- catch (TimeoutException)
- {
- // Timeout ocorreu, o que significa que não foi possível escrever os dados no tempo estipulado
- if (DebugMode)
- Console.WriteLine("Erro: Timeout ao tentar enviar dados.");
- return false;
- }
- catch (InvalidOperationException ex)
- {
- // A porta não estava aberta ou algo relacionado a uma falha de operação na porta
- if (DebugMode)
- Console.WriteLine("Erro: A porta serial não está aberta ou houve uma falha na operação. " + ex.Message);
- return false;
- }
- catch (UnauthorizedAccessException ex)
- {
- // Caso a porta serial esteja sendo acessada por outro processo
- if (DebugMode)
- Console.WriteLine("Erro: Acesso negado à porta serial. " + ex.Message);
- return false;
- }
- catch (Exception ex)
- {
- // Captura outros tipos de erro inesperados
- if (DebugMode)
- Console.WriteLine("Erro inesperado: " + ex.Message);
- return false;
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Interlocked.Increment(
+ ref _serialWriteErrors
+ );
+
+ RecordError(
+ "[SerialService.WriteAsync] " +
+ SafePortName(porta) + ": " + ex.Message
+ );
+
+ return false;
+ }
+ finally
+ {
+ if (acquired)
+ gate.Release();
+ }
}
}
+ // ============================================================
+ // VARREDURA PRINCIPAL
+ // ============================================================
- public static async Task RealizarVarreduraPortasUSB()
+ public static Task RealizarVarreduraPortasUSB()
{
- if (VarreduraEmAndamento || Variaveis.Fechando)
+ return RealizarVarreduraPortasUSB(
+ CancellationToken.None
+ );
+ }
+
+ public static async Task RealizarVarreduraPortasUSB(
+ CancellationToken cancellationToken)
+ {
+ if (Variaveis.Fechando ||
+ Volatile.Read(ref _isClosing) == 1)
{
return;
}
- VarreduraEmAndamento = true;
+ EnsureLifetimeAvailable();
- try
+ CancellationToken lifetimeToken;
+
+ lock (_lifecycleLock)
+ lifetimeToken = _lifetimeCts.Token;
+
+ using (var linkedCts =
+ CancellationTokenSource.CreateLinkedTokenSource(
+ cancellationToken,
+ lifetimeToken))
{
- var dispositivosMapeados = DispositivosMapeados.ToList();
+ bool entered;
- var Portas = SerialPort.GetPortNames();
- var PortasNaoMapeadas = Portas.Where(x => !dispositivosMapeados.Select(y => y.Endereco).Contains(x)).ToList();
- List PortasSemResposta = new List();
-
- for (int i = 0; i < PortasNaoMapeadas.Count(); i++)
- {
- string PortaCom = PortasNaoMapeadas[i].ToString();
- AtualizarConsole("Procurando dispositivos conectados à porta " + PortaCom + "...");
- SerialPort _Porta = new SerialPort() { PortName = PortaCom, BaudRate = 115200 };
-
- if (!LoRaBaseService.Iniciado && Variaveis.IsAgroMonitor)
- {
- bool LoRa = await ProcurarDispositivoLoRa(_Porta);
- if (LoRa)
- {
- continue;
- }
- }
-
- if (!GPSService.Iniciado)
- {
- bool GPSEncontrado = await ProcurarDispositivoGPS(_Porta);
- if (GPSEncontrado)
- {
- continue;
- }
- }
-
- if (Variaveis.IsAgroMonitor) continue;
-
- if (CanManager.TipoServico == CanServiceTipo.Serial && !CanManager.CanService.Iniciado)
- {
- bool dispositivoCan = await ProcurarDispositivosCAN(_Porta);
- if (dispositivoCan)
- {
- AtualizarConsole("Dispositivo encontrado no barramento CAN");
- continue;
- }
- }
- }
-
- if (Variaveis.IsAgroMonitor) return;
-
-
- ProcurarDispositivosUSB();
-
- ProcurarDispositivosEthernet();
-
- await ProcurarDispositivosCAN();
-
- var DispositivosRemovidos = dispositivosMapeados.Where(x => x.Tipo == TipoConexao.PortaCOM && (!Portas.Contains(x.Endereco))).ToList();
- foreach (var Dispositivo in DispositivosRemovidos)
- {
- DispositivosMapeados.Remove(Dispositivo);
- switch (Dispositivo.Dispositivo)
- {
- case T_Code.Gps:
- GPSService.PortaGPS?.Close();
- GPSService.PortaGPS = null;
- break;
- case T_Code.Lra:
- LoRaBaseService._PortaLoRa?.Close();
- LoRaBaseService._PortaLoRa = null;
- break;
- }
- }
-
-
-
- AtualizarDispositivos();
- }
- catch (Exception ex)
- {
- Console.WriteLine("Erro ao realizar varredura de portas: " + ex.Message);
- }
- finally
- {
- VarreduraEmAndamento = false;
-
- RealizarVerificacaoDeRotina();
-
- AtualizarConsole("Iteração finalizada, aguardando a próxima...");
- }
- }
-
- private static void RealizarVerificacaoDeRotina()
- {
- Task.Run(async () =>
- {
- await FuncoesGlobais.SafeExecuteAsync(async () =>
- {
- try
- {
- await VersionamentoService.AtualizarArquivoVersionamento(false);
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Erro ao atualizar arquivo de versionamento: {ex.Message}");
- }
- });
- });
-
- Task.Run(async () =>
- {
- await FuncoesGlobais.SafeExecuteAsync(async () =>
- {
- try
- {
- await SyncDataService.SincronizarArquivosComServidor();
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Erro ao sincronizar arquivos com o servidor: {ex.Message}");
- }
- });
- });
- }
-
- public static string ReadLineWithTimeout(SerialPort porta, int timeout)
- {
- using (CancellationTokenSource cts = new CancellationTokenSource())
- {
try
{
- Task task = Task.Run(() =>
- {
- if (porta.IsOpen) // Verifica se a porta está aberta
- {
- return porta.ReadLine();
- }
- throw new IOException("A porta serial não está aberta.");
- }, cts.Token);
-
- if (task.Wait(timeout))
- {
- return task.Result;
- }
- else
- {
- cts.Cancel();
- return ""; // Retorna vazio se o timeout for atingido
- }
+ entered = await _scanGate
+ .WaitAsync(0, linkedCts.Token)
+ .ConfigureAwait(false);
}
- catch (IOException ex)
+ catch (OperationCanceledException)
{
- Console.WriteLine($"Erro de I/O: {ex.Message}");
- return ""; // Ou trate como necessário
+ return;
+ }
+
+ if (!entered)
+ {
+ Interlocked.Increment(ref _scanSkipped);
+ return;
+ }
+
+ Interlocked.Exchange(ref _isScanning, 1);
+ Interlocked.Increment(ref _scanStarted);
+
+ long generation =
+ Interlocked.Increment(
+ ref _scanGeneration
+ );
+
+ long startMono = Stopwatch.GetTimestamp();
+
+ Interlocked.Exchange(
+ ref _lastScanStartMono,
+ startMono
+ );
+
+ lock (_lifecycleLock)
+ _currentScanCts = linkedCts;
+
+ try
+ {
+ await ExecutarVarreduraAsync(
+ generation,
+ linkedCts.Token
+ ).ConfigureAwait(false);
+
+ Interlocked.Increment(ref _scanCompleted);
+ }
+ catch (OperationCanceledException)
+ {
+ Interlocked.Increment(ref _scanCanceled);
}
catch (Exception ex)
{
- Console.WriteLine($"Erro: {ex.Message}");
- return ""; // Ou trate como necessário
- }
- }
- }
+ Interlocked.Increment(ref _scanErrors);
- public static void AtualizarDispositivos()
- {
- bool PrimeiraExecucao = !Variaveis.DispositivosConectados.Any();
- DispositivosConexaoInicial.ForEach(Dispositivo =>
- {
- IDispositivosService dispositivoService = Variaveis.DispositivosConectados.FirstOrDefault(x => x.Dispositivo == Dispositivo);
-
- if (dispositivoService == null)
- {
- dispositivoService = DispositivosServiceFactory.CreateDispositivoService(
- Dispositivo,
- "Nome: " + Enum.GetName(typeof(T_Code), Dispositivo),
- "Descrição: " + string.Join(", ", DispositivosMapeados.Where(x => x.Dispositivo == Dispositivo).Select(x => x.Mod_ID).ToArray())
+ RecordError(
+ "[SerialService.Scan] " + ex
);
- dispositivoService.CarregarParametrosModulos();
- Variaveis.DispositivosConectados.Add(dispositivoService);
- dispositivoService.Dados.IniciarRegistrador();
}
- });
- if (PrimeiraExecucao)
- {
- //OperacaoModel.CarregarParametrosOperacaoPadrao(ModoOperacao.MapaGPS, true);
- Func func = new Func(() =>
+ finally
{
- frmInstancial.frmPrincipal.cmbModoOperacao.SelectedIndex = (int)ModoOperacao.MapaGPS;
- return true;
- });
- FuncoesGlobais.ExecutarMetodoComVerificacaoCrossThread(frmInstancial.frmPrincipal.cmbModoOperacao, func);
- }
+ long endMono = Stopwatch.GetTimestamp();
+ double duration = ElapsedMs(
+ startMono,
+ endMono
+ );
- //DispositivosMapeados.ForEach(d => d.AtualizarDados());
+ lock (_metricsTextLock)
+ {
+ _lastScanDurationMs = duration;
+
+ if (duration > _maxScanDurationMs)
+ _maxScanDurationMs = duration;
+ }
+
+ Interlocked.Exchange(
+ ref _lastScanEndMono,
+ endMono
+ );
+
+ lock (_lifecycleLock)
+ {
+ if (ReferenceEquals(
+ _currentScanCts,
+ linkedCts))
+ {
+ _currentScanCts = null;
+ }
+ }
+
+ Interlocked.Exchange(ref _isScanning, 0);
+ _scanGate.Release();
+
+ TriggerMaintenance();
+ AtualizarConsole(
+ "Iteração finalizada, aguardando a próxima..."
+ );
+ }
+ }
}
- private static async Task ProcurarDispositivosCAN(SerialPort Porta = null)
+ private static async Task ExecutarVarreduraAsync(
+ long generation,
+ CancellationToken cancellationToken)
{
- AtualizarConsole($"{Porta?.PortName ?? CanManager.CanService._portName ?? "CAN"} - Procurando dispositivos no barramento CAN");
+ cancellationToken.ThrowIfCancellationRequested();
- bool porta_can = CanManager.CanService.PortaIsCan(Porta);
- bool inicializou = !CanManager.CanService.Inicializar(250);
- if (!porta_can || inicializou)
+ /*
+ * Garante que SEN, ATU e MVD existam antes da primeira
+ * consulta CAN. Assim a descoberta não precisa esperar
+ * uma segunda varredura para encontrar os módulos.
+ */
+ if (!Variaveis.IsAgroMonitor)
{
- AtualizarConsole($"Adaptador {CanManager.CanService._portName ?? "CAN"} não conectado!");
- var dispositivosRemover = DispositivosMapeados.Where(x => DispositivosCan.Contains(x.Dispositivo)).ToList();
- foreach (var d in dispositivosRemover)
+ await AtualizarDispositivosAsync(
+ cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ List mappedSnapshot =
+ GetDispositivosMapeadosSnapshot();
+
+ string[] ports = SerialPort.GetPortNames()
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(PortSortKey)
+ .ToArray();
+
+ Interlocked.Add(
+ ref _portsEnumerated,
+ ports.Length
+ );
+
+ HashSet mappedComPorts =
+ new HashSet(
+ mappedSnapshot
+ .Where(x =>
+ x != null &&
+ x.Tipo == TipoConexao.PortaCOM &&
+ !string.IsNullOrWhiteSpace(
+ x.Endereco
+ ))
+ .Select(x => x.Endereco),
+ StringComparer.OrdinalIgnoreCase
+ );
+
+ string[] unmappedPorts = ports
+ .Where(x => !mappedComPorts.Contains(x))
+ .ToArray();
+
+ foreach (string portName in unmappedPorts)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (generation !=
+ Interlocked.Read(ref _scanGeneration))
{
- DispositivosMapeados.Remove(d);
+ return;
}
+
+ await ProbePortAsync(
+ portName,
+ cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ if (!Variaveis.IsAgroMonitor)
+ {
+ ProcurarDispositivosUSB();
+ ProcurarDispositivosEthernet();
+
+ /*
+ * Se o CAN já está iniciado, apenas consulta os módulos.
+ * Se estiver em Ethernet, Inicializar() resolve o transporte
+ * sem depender de uma porta COM candidata.
+ */
+ if (CanManager.TipoServico !=
+ CanServiceTipo.Serial ||
+ CanManager.CanService.Iniciado)
+ {
+ await ProcurarDispositivosCAN(
+ null,
+ cancellationToken
+ ).ConfigureAwait(false);
+ }
+ }
+
+ await RemoverDispositivosAusentesAsync(
+ ports,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ await AtualizarDispositivosAsync(
+ cancellationToken
+ ).ConfigureAwait(false);
+ }
+
+ private static async Task ProbePortAsync(
+ string portName,
+ CancellationToken cancellationToken)
+ {
+ Interlocked.Increment(ref _portsProbed);
+
+ AtualizarConsole(
+ "Procurando dispositivos conectados à porta " +
+ portName + "..."
+ );
+
+ SerialPort candidate = null;
+ bool claimed = false;
+
+ try
+ {
+ candidate = CriarPortaCandidata(portName);
+
+ if (Variaveis.IsAgroMonitor &&
+ !LoRaBaseService.Iniciado)
+ {
+ if (await ProcurarDispositivoLoRa(
+ candidate,
+ cancellationToken)
+ .ConfigureAwait(false))
+ {
+ claimed = true;
+ Interlocked.Increment(ref _loraFound);
+ return;
+ }
+ }
+
+ if (!GPSService.Iniciado)
+ {
+ if (await ProcurarDispositivoGPS(
+ candidate,
+ cancellationToken)
+ .ConfigureAwait(false))
+ {
+ Interlocked.Increment(ref _gpsFound);
+
+ /*
+ * O GPSService robusto cria sua própria instância
+ * de SerialPort. A candidata não é transferida.
+ */
+ return;
+ }
+ }
+
+ if (Variaveis.IsAgroMonitor)
+ return;
+
+ if (CanManager.TipoServico ==
+ CanServiceTipo.Serial &&
+ !CanManager.CanService.Iniciado)
+ {
+ if (await ProcurarDispositivosCAN(
+ candidate,
+ cancellationToken)
+ .ConfigureAwait(false))
+ {
+ claimed = true;
+ Interlocked.Increment(
+ ref _canAdapterFound
+ );
+
+ AtualizarConsole(
+ "Dispositivo encontrado no barramento CAN"
+ );
+
+ return;
+ }
+ }
+ }
+ catch (UnauthorizedAccessException)
+ {
+ Interlocked.Increment(ref _portsBusy);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (IOException ex)
+ {
+ Interlocked.Increment(ref _portsProbeErrors);
+
+ RecordError(
+ "[SerialService.ProbePort] " +
+ portName + ": " + ex.Message
+ );
+ }
+ catch (Exception ex)
+ {
+ Interlocked.Increment(ref _portsProbeErrors);
+
+ RecordError(
+ "[SerialService.ProbePort] " +
+ portName + ": " + ex.Message
+ );
+ }
+ finally
+ {
+ if (!claimed && candidate != null)
+ CloseAndDispose(candidate);
+ }
+ }
+
+ // ============================================================
+ // DETECÇÃO DE DISPOSITIVOS
+ // ============================================================
+
+ private static async Task ProcurarDispositivosCAN(
+ SerialPort porta = null,
+ CancellationToken cancellationToken =
+ default(CancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ string name =
+ porta?.PortName ??
+ CanManager.CanService._portName ??
+ "CAN";
+
+ AtualizarConsole(
+ name +
+ " - Procurando dispositivos no barramento CAN"
+ );
+
+ /*
+ * Uma candidata que não é CAN é simplesmente ignorada.
+ * Nunca removemos mapeamentos CAN por causa desse teste.
+ */
+ if (porta != null &&
+ !CanManager.CanService.PortaIsCan(porta))
+ {
return false;
}
- if (true && Variaveis.OperacaoEmAndamento.DispSen != null && !Variaveis.OperacaoEmAndamento.DispSen.Dados.Conectado)
+ if (!CanManager.CanService.Iniciado)
{
- AtualizarConsole($"{CanManager.CanService._portName} - Procurando dispositivo SEN");
- bool DispSen = await Variaveis.OperacaoEmAndamento.DispSen.Dados.VerificaDispositivoConectado();
- if (DispSen)
+ bool initialized =
+ CanManager.CanService.Inicializar(250);
+
+ if (!initialized)
{
+ AtualizarConsole(
+ "Adaptador " + name + " não conectado!"
+ );
+
+ return false;
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Variaveis.OperacaoEmAndamento.DispSen != null &&
+ !Variaveis.OperacaoEmAndamento
+ .DispSen
+ .Dados
+ .Conectado)
+ {
+ AtualizarConsole(
+ name + " - Procurando dispositivo SEN"
+ );
+
+ bool found =
+ await Variaveis.OperacaoEmAndamento
+ .DispSen
+ .Dados
+ .VerificaDispositivoConectado()
+ .ConfigureAwait(false);
+
+ if (found)
AtualizarConsole("Dispositivo SEN encontrado");
- }
}
-
- if (true && Variaveis.OperacaoEmAndamento.DispAtu != null && !Variaveis.OperacaoEmAndamento.DispAtu.Dados.Conectado)
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Variaveis.OperacaoEmAndamento.DispAtu != null &&
+ !Variaveis.OperacaoEmAndamento
+ .DispAtu
+ .Dados
+ .Conectado)
{
- AtualizarConsole($"{CanManager.CanService._portName} - Procurando dispositivo ATU");
- bool DispAtu = await Variaveis.OperacaoEmAndamento.DispAtu.Dados.VerificaDispositivoConectado();
- if (DispAtu)
- {
+ AtualizarConsole(
+ name + " - Procurando dispositivo ATU"
+ );
+
+ bool found =
+ await Variaveis.OperacaoEmAndamento
+ .DispAtu
+ .Dados
+ .VerificaDispositivoConectado()
+ .ConfigureAwait(false);
+
+ if (found)
AtualizarConsole("Dispositivo ATU encontrado");
- }
}
- if (true && Variaveis.OperacaoEmAndamento.DispMvd != null && !MKS057DCanService.Referenciando && (!MKS057DCanService.Iniciado || MKS057DCanService.DadosLeitura.Count(x => x.Iniciado) < (Variaveis.OperacaoEmAndamento.DispMvd?.Dados?.Modulos?.Count() ?? 4)))
+ cancellationToken.ThrowIfCancellationRequested();
+
+ int expectedModules =
+ Variaveis.OperacaoEmAndamento
+ .DispMvd?
+ .Dados?
+ .Modulos?
+ .Count() ?? 4;
+
+ if (Variaveis.OperacaoEmAndamento.DispMvd != null &&
+ !MKS057DCanService.Referenciando &&
+ (
+ !MKS057DCanService.Iniciado ||
+ MKS057DCanService.DadosLeitura
+ .Count(x => x.Iniciado) <
+ expectedModules
+ ))
{
- AtualizarConsole($"{CanManager.CanService._portName} - Procurando dispositivos MKS");
- bool DispMks = await MKS057DCanService.VerificaDispositivoConectado();
- if (DispMks)
- {
+ AtualizarConsole(
+ name + " - Procurando dispositivos MKS"
+ );
+
+ bool found =
+ await MKS057DCanService
+ .VerificaDispositivoConectado()
+ .ConfigureAwait(false);
+
+ if (found)
AtualizarConsole("Dispositivo MKS encontrado");
- }
}
- if (true && Variaveis.OperacaoEmAndamento.DispMvd != null && (!OIDCanService.Iniciado || OIDCanService.DadosLeitura.Count(x => x.Iniciado) < (Variaveis.OperacaoEmAndamento.DispMvd?.Dados?.Modulos?.Count() ?? 4)))
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Variaveis.OperacaoEmAndamento.DispMvd != null &&
+ (
+ !OIDCanService.Iniciado ||
+ OIDCanService.DadosLeitura
+ .Count(x => x.Iniciado) <
+ expectedModules
+ ))
{
- AtualizarConsole($"{CanManager.CanService._portName} - Procurando dispositivo OID");
- bool DispOid = await OIDCanService.VerificaDispositivoConectado();
- if (DispOid)
- {
+ AtualizarConsole(
+ name + " - Procurando dispositivo OID"
+ );
+
+ bool found =
+ await OIDCanService
+ .VerificaDispositivoConectado()
+ .ConfigureAwait(false);
+
+ if (found)
AtualizarConsole("Dispositivo OID encontrado");
- }
}
- if (true && !DalyBMSService.Iniciado)
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!DalyBMSService.Iniciado)
{
- AtualizarConsole($"{CanManager.CanService._portName} - Procurando dispositivo BAT");
- bool DispBat = await DalyBMSService.VerificaDispositivoConectado();
- if (DispBat)
- {
+ AtualizarConsole(
+ name + " - Procurando dispositivo BAT"
+ );
+
+ bool found =
+ await DalyBMSService
+ .VerificaDispositivoConectado()
+ .ConfigureAwait(false);
+
+ if (found)
AtualizarConsole("Dispositivo BAT encontrado");
- }
}
return CanManager.CanService.Iniciado;
@@ -463,219 +1014,1458 @@ namespace AgroBase.Services
private static void ProcurarDispositivosUSB()
{
- if (!Variaveis.IsAgroMonitor)
+ if (Variaveis.IsAgroMonitor)
+ return;
+
+ if (GeneralJoystick.JoystickConectado == null)
+ GeneralJoystick.AtualizaDispositivo();
+
+ var cameras = CameraWorkerService.ListaCameras;
+
+ if (cameras == null)
+ return;
+
+ foreach (var camera in cameras)
{
- if (false && !KinectService.Iniciado)
+ var health =
+ HealthWorkerService.ModulosSaude
+ .FirstOrDefault(
+ x => x.modulo == camera.dispositivo
+ );
+
+ if (health == null)
+ continue;
+
+ DispositivoDetalhesModel mapped =
+ GetDispositivosMapeadosSnapshot()
+ .FirstOrDefault(
+ x => x.Dispositivo ==
+ camera.dispositivo
+ );
+
+ bool connected =
+ health.status !=
+ StatusModulo.Desconectado;
+
+ if (mapped == null && connected)
{
- KinectService.InicializarKinect();
- }
-
- if (GeneralJoystick.JoystickConectado == null)
- {
- GeneralJoystick.AtualizaDispositivo();
- }
-
- var _Cameras = CameraWorkerService.ListaCameras;
- foreach (var cam in _Cameras)
- {
- var disp = HealthWorkerService.ModulosSaude.FirstOrDefault(x => x.modulo == cam.dispositivo);
- if (disp == null) continue;
-
- var added = DispositivosMapeados.FirstOrDefault(x => x.Dispositivo == cam.dispositivo);
- bool connected = disp.status != StatusModulo.Desconectado;
-
- if (added == null && connected)
- {
- DispositivosMapeados.Add(new DispositivoDetalhesModel()
+ AdicionarOuAtualizarDispositivoMapeado(
+ new DispositivoDetalhesModel
{
- Dispositivo = cam.dispositivo,
+ Dispositivo =
+ camera.dispositivo,
Endereco = "USB",
- Versao = cam.versao
- });
- }
- else if (added != null && !connected)
- {
- DispositivosMapeados.Remove(added);
- }
+ Versao = camera.versao
+ }
+ );
}
- /*var _dMap = DispositivosMapeados.Where(x => x.Tipo == TipoConexao.USB).ToList();
- foreach (var cam in _dMap)
+ else if (mapped != null && !connected)
{
- bool camPresente = _Cameras.Any(x => x.Dispositivo == cam.Dispositivo);
- if (!camPresente)
- {
- DispositivosMapeados.Remove(cam);
- }
- }*/
+ RemoverDispositivoMapeado(mapped);
+ }
}
}
- private static async Task ProcurarDispositivoLoRa(SerialPort porta)
+ private static async Task ProcurarDispositivoLoRa(
+ SerialPort porta,
+ CancellationToken cancellationToken)
{
- AtualizarConsole($"{porta.PortName} - Procurando dispositivo LRA");
- bool Encontrado = await LoRaBaseService.VerificaPortaLoRa(porta);
- if (Encontrado)
+ if (porta == null)
+ return false;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ AtualizarConsole(
+ porta.PortName +
+ " - Procurando dispositivo LRA"
+ );
+
+ bool found =
+ await LoRaBaseService
+ .VerificaPortaLoRa(porta)
+ .ConfigureAwait(false);
+
+ if (found)
{
AtualizarConsole("Dispositivo LRA encontrado");
return true;
}
- porta.Close();
+
+ ClosePortOnly(porta);
return false;
}
- private static async Task ProcurarDispositivoGPS(SerialPort Porta)
+ private static async Task ProcurarDispositivoGPS(
+ SerialPort porta,
+ CancellationToken cancellationToken)
{
- SerialPort _porta = new SerialPort(Porta.PortName, 115200);
+ if (porta == null ||
+ string.IsNullOrWhiteSpace(porta.PortName))
+ {
+ return false;
+ }
+
+ SerialPort probe =
+ CriarPortaCandidata(porta.PortName);
+
try
{
- _porta.ReadTimeout = 500;
- _porta.WriteTimeout = 500;
- _porta.Handshake = Handshake.None;
- _porta.DtrEnable = false;
- _porta.RtsEnable = false;
+ probe.ReadTimeout = 250;
+ probe.WriteTimeout = 500;
+ probe.Open();
- _porta.Open();
- await Task.Delay(100);
- var nmea = $"gngga com3 1\r\n";
- _porta.Write(Encoding.ASCII.GetBytes(nmea), 0, nmea.Length);
- AtualizarConsole($"{_porta.PortName} - Procurando dispositivo GPS");
- await Task.Delay(1000);
+ try { probe.DiscardInBuffer(); }
+ catch { }
- string Recebido = "";
+ const string command =
+ "gngga com3 1\r\n";
- if (string.IsNullOrEmpty(Recebido))
- {
- //Recebido = Porta.ReadLine();
- byte[] buffer = LerDadosDaPortaSerial(_porta, 15);
- Recebido = Encoding.ASCII.GetString(buffer);
- }
+ byte[] commandBytes =
+ Encoding.ASCII.GetBytes(command);
- if (
- (Recebido.Contains("$GPTXT") || Recebido.Contains("$GPRMC") || Recebido.Contains("$GPGGA") || Recebido.Contains("$GPGLL")) ||
- (Recebido.Contains("$GNGGA") || Recebido.Contains("$GPVTG") || Recebido.Contains("$GPGSV") || Recebido.Contains("$GNTHS"))
- )
- {
- GPSService.AtualizarPortaCOM(_porta);
- AtualizarConsole("Dispositivo GPS encontrado");
- return true;
- }
+ probe.Write(
+ commandBytes,
+ 0,
+ commandBytes.Length
+ );
+
+ AtualizarConsole(
+ probe.PortName +
+ " - Procurando dispositivo GPS"
+ );
+
+ byte[] received =
+ await LerAteCondicaoAsync(
+ probe,
+ maxBytes: 4096,
+ timeout: 1600,
+ predicate: IsGpsPayload,
+ cancellationToken:
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (!IsGpsPayload(received))
+ return false;
+
+ /*
+ * O novo GPSService copia a configuração da porta,
+ * cria sua própria instância e gerencia o lifecycle.
+ */
+ GPSService.AtualizarPortaCOM(probe);
+
+ AtualizarConsole(
+ "Dispositivo GPS encontrado"
+ );
+
+ return true;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ Interlocked.Increment(ref _portsBusy);
return false;
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
catch (Exception ex)
{
+ RecordError(
+ "[SerialService.ProbeGPS] " +
+ porta.PortName + ": " + ex.Message
+ );
+
return false;
}
finally
{
- _porta.Close();
+ CloseAndDispose(probe);
}
}
private static void ProcurarDispositivosEthernet()
{
- var Lvx = HealthWorkerService.ModulosSaude.FirstOrDefault(x => x.modulo == T_Code.Lvx);
- if (Lvx != null)
- {
- var added = DispositivosMapeados.FirstOrDefault(x => x.Dispositivo == Lvx.modulo);
- bool connected = Lvx.status != StatusModulo.Desconectado;
+ var livox =
+ HealthWorkerService.ModulosSaude
+ .FirstOrDefault(
+ x => x.modulo == T_Code.Lvx
+ );
- if (added == null && connected)
- {
- DispositivosMapeados.Add(new DispositivoDetalhesModel()
+ if (livox == null)
+ return;
+
+ DispositivoDetalhesModel mapped =
+ GetDispositivosMapeadosSnapshot()
+ .FirstOrDefault(
+ x => x.Dispositivo == livox.modulo
+ );
+
+ bool connected =
+ livox.status !=
+ StatusModulo.Desconectado;
+
+ if (mapped == null && connected)
+ {
+ AdicionarOuAtualizarDispositivoMapeado(
+ new DispositivoDetalhesModel
{
- Dispositivo = Lvx.modulo,
- Endereco = LivoxManagerProcess.DadosLeitura.lidar_ip,
- Versao = LivoxManagerProcess.DadosLeitura.firmware_version,
- Mod_ID = LivoxManagerProcess.DadosLeitura.dev_type,
- });
- }
- else if (added != null && !connected)
- {
- DispositivosMapeados.Remove(added);
- }
+ Dispositivo = livox.modulo,
+ Endereco =
+ LivoxManagerProcess
+ .DadosLeitura
+ .lidar_ip,
+ Versao =
+ LivoxManagerProcess
+ .DadosLeitura
+ .firmware_version,
+ Mod_ID =
+ LivoxManagerProcess
+ .DadosLeitura
+ .dev_type
+ }
+ );
+ }
+ else if (mapped != null && !connected)
+ {
+ RemoverDispositivoMapeado(mapped);
}
}
- public static byte[] LerDadosDaPortaSerial(SerialPort porta, int tamanhoEsperado, int Timeout = 500)
+ // ============================================================
+ // REMOÇÃO E ATUALIZAÇÃO DE MODELOS
+ // ============================================================
+
+ private static async Task RemoverDispositivosAusentesAsync(
+ string[] currentPorts,
+ CancellationToken cancellationToken)
{
- DateTime inicio = DateTime.Now;
- List buffer = new List();
- while (buffer.Count < tamanhoEsperado && inicio.AddMilliseconds(Timeout) > DateTime.Now)
+ var current =
+ new HashSet(
+ currentPorts ?? new string[0],
+ StringComparer.OrdinalIgnoreCase
+ );
+
+ List mappedCom =
+ GetDispositivosMapeadosSnapshot()
+ .Where(x =>
+ x != null &&
+ x.Tipo == TipoConexao.PortaCOM &&
+ !string.IsNullOrWhiteSpace(x.Endereco))
+ .ToList();
+
+ foreach (DispositivoDetalhesModel present in mappedCom)
{
- if (porta == null)
+ if (current.Contains(present.Endereco))
{
- break;
- }
- int bytesDisponiveis = porta.BytesToRead;
- if (bytesDisponiveis > 0)
- {
- byte[] tempBuffer = new byte[bytesDisponiveis];
- int bytesLidos = porta.Read(tempBuffer, 0, bytesDisponiveis);
- buffer.AddRange(tempBuffer.Take(bytesLidos));
+ int ignored;
+ _missingPortConfirmations.TryRemove(
+ BuildMissingPortKey(present),
+ out ignored
+ );
}
}
- return buffer.ToArray();
- }
- public static async Task LerDadosDaPortaSerialAsync(SerialPort porta, int tamanhoEsperado, int Timeout = 500)
- {
- DateTime inicio = DateTime.Now;
- List buffer = new List