Ajustes para aumento de robustez entre base e rover
This commit is contained in:
parent
433c88a196
commit
4de10bdb67
|
|
@ -517,6 +517,7 @@
|
|||
<DependentUpon>frmSimulacaoMapeamentoVisual.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Models\AlarmeModel.cs" />
|
||||
<Compile Include="Models\BaseLinkState.cs" />
|
||||
<Compile Include="Models\Components\AsyncTaskTimerModel.cs" />
|
||||
<Compile Include="Models\BLD300RModel.cs" />
|
||||
<Compile Include="Models\CameraCaminhoModel.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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,636 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace AgroBase.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool CriticalChannelHealthy
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!BrokerConnected)
|
||||
return false;
|
||||
|
||||
double lastCriticalAge = Math.Min(
|
||||
LastHeartbeatAgeMs,
|
||||
Math.Min(LastRtcmAgeMs, LastCommandAgeMs)
|
||||
);
|
||||
|
||||
return lastCriticalAge <= Volatile.Read(ref _criticalChannelTimeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A descoberta só deve ser publicada quando o broker está conectado,
|
||||
/// a base ainda não foi reconhecida e o intervalo de retry venceu.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inicia uma nova sessão lógica do rover.
|
||||
/// Toda confirmação de descoberta anterior é invalidada.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registra envio de discovery e retorna a sequência utilizada.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirma a descoberta somente se o sessionId recebido pertencer
|
||||
/// à sessão atual. Retorna false para ACK antigo ou inválido.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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<IDispositivosService> DispositivosConectados { get; set; } = new List<IDispositivosService>();
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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()
|
||||
/// <summary>
|
||||
/// Nome antigo preservado para compatibilidade. Novos pontos de startup
|
||||
/// devem aguardar IniciarMqttAsync diretamente.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
/// <summary>
|
||||
/// Inicializa deterministicamente os clientes MQTT e seus tópicos.
|
||||
/// Uma segunda chamada encerra por completo a geração anterior antes
|
||||
/// de criar novos clientes.
|
||||
/// </summary>
|
||||
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("<id>", VariaveisEquipamento.Parametros.serial_number));
|
||||
await MqttServiceBase.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttParametros.Replace("<id>", VariaveisEquipamento.Parametros.serial_number));
|
||||
await MqttServiceBase.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttHeartbeat.Replace("<id>", 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("<id>", VariaveisEquipamento.Parametros.serial_number), true, 1, async (message) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(message.Mensagem)) return;
|
||||
try
|
||||
{
|
||||
string json = message.Mensagem;
|
||||
var cmd = JsonConvert.DeserializeObject<OperacaoComandoBaseModel>(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("<id>", 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("<id>", roverId),
|
||||
inscrever: true,
|
||||
mensagensManter: 2,
|
||||
callback: message =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message.Mensagem))
|
||||
return Task.CompletedTask;
|
||||
|
||||
try
|
||||
{
|
||||
OperacaoComandoBaseModel cmd =
|
||||
JsonConvert.DeserializeObject<OperacaoComandoBaseModel>(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<GPSModel>(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("<id>", 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("<id>", 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<GPSModel>(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();
|
||||
|
||||
/// <summary>
|
||||
/// Propriedade mantida por compatibilidade. Novos callbacks devem usar
|
||||
/// AtualizarPosicaoBase e leituras concorrentes devem usar
|
||||
/// ObterPosicaoBaseSnapshot.
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,62 +1,443 @@
|
|||
using AgroBase.Models;
|
||||
using AgroBase.Models;
|
||||
using AgroBase.Models.Operacoes;
|
||||
using AgroBase.Services;
|
||||
using OperationControl.Windows;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static AgroBase.Models.Enums;
|
||||
|
||||
namespace OperationControl.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Construtor/lifecycle principal da aplicação OperationControl.
|
||||
///
|
||||
/// Regras:
|
||||
/// - MQTT é inicializado de forma aguardável antes dos loops;
|
||||
/// - heartbeat, posição da base e supervisão dos rovers usam timers separados;
|
||||
/// - não usa System.Timers.Timer com callbacks async ignorados;
|
||||
/// - inicialização é idempotente;
|
||||
/// - shutdown cancela timers, MQTT e UDP de forma ordenada;
|
||||
/// - mock é aguardável e isolado.
|
||||
/// </summary>
|
||||
public class AppShell
|
||||
{
|
||||
public static readonly bool Mock = false;
|
||||
public static readonly bool AddRover = false;
|
||||
|
||||
private readonly System.Timers.Timer tmrComunicacao = new(1000) { AutoReset = true };
|
||||
private int tmrComunicacaoTicks = 0;
|
||||
private bool tmrComunicacaoTicking = false;
|
||||
private readonly SemaphoreSlim _lifecycleLock =
|
||||
new SemaphoreSlim(1, 1);
|
||||
|
||||
private CancellationTokenSource _appCts;
|
||||
|
||||
private AsyncTaskTimerModel _tmrHeartbeatBase;
|
||||
private AsyncTaskTimerModel _tmrAtualizaRovers;
|
||||
private AsyncTaskTimerModel _tmrPosicaoBase;
|
||||
private AsyncTaskTimerModel _tmrMockUi;
|
||||
|
||||
private Task _mockTask;
|
||||
|
||||
private bool _initialized;
|
||||
private bool _closing;
|
||||
|
||||
public MainWindow Main { get; private set; }
|
||||
public DockWindow Dock { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compatibilidade com o contrato antigo.
|
||||
/// Para startup determinístico, prefira:
|
||||
/// await InicializarAsync();
|
||||
/// </summary>
|
||||
public void Inicializar()
|
||||
{
|
||||
Show();
|
||||
Forget(
|
||||
InicializarAsync(),
|
||||
"Inicializar AppShell"
|
||||
);
|
||||
}
|
||||
|
||||
APIService.IniciarRotinas();
|
||||
Variaveis.IniciarMQTT();
|
||||
Variaveis.IniciarUDP();
|
||||
public async Task InicializarAsync(
|
||||
CancellationToken cancellationToken =
|
||||
default(CancellationToken))
|
||||
{
|
||||
await _lifecycleLock
|
||||
.WaitAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
tmrComunicacao.Elapsed += (_, __) => tmrComunicacao_Elapsed();
|
||||
tmrComunicacao.Start();
|
||||
try
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_closing = false;
|
||||
|
||||
_appCts = CancellationTokenSource
|
||||
.CreateLinkedTokenSource(
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
Show();
|
||||
|
||||
APIService.IniciarRotinas();
|
||||
|
||||
/*
|
||||
* O MQTT novo precisa estar pronto antes dos timers.
|
||||
* Isso garante que tópicos, clientes e publisher RTCM
|
||||
* existam antes do heartbeat e da posição da base.
|
||||
*/
|
||||
await Variaveis
|
||||
.IniciarMqttAsync(_appCts.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Variaveis.IniciarUDP();
|
||||
|
||||
CriarTimers();
|
||||
IniciarTimers();
|
||||
|
||||
if (Mock && AddRover)
|
||||
{
|
||||
_mockTask = Task.Run(
|
||||
() => ConfigurarMockAsync(
|
||||
_appCts.Token
|
||||
),
|
||||
_appCts.Token
|
||||
);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await EncerrarInternoAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lifecycleLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void Show()
|
||||
{
|
||||
// Aqui você decide qual janela é a inicial de fato
|
||||
Main = new MainWindow();
|
||||
Dock = new DockWindow();
|
||||
if (Main == null)
|
||||
Main = new MainWindow();
|
||||
|
||||
if (Dock == null)
|
||||
Dock = new DockWindow();
|
||||
}
|
||||
|
||||
private void CriarTimers()
|
||||
{
|
||||
int intervaloPosicaoMs =
|
||||
Math.Max(
|
||||
1000,
|
||||
VariaveisEquipamento
|
||||
.TempoEntrePingsConexao
|
||||
);
|
||||
|
||||
_tmrHeartbeatBase =
|
||||
new AsyncTaskTimerModel(
|
||||
"base.heartbeat",
|
||||
async cancellationToken =>
|
||||
{
|
||||
await VariaveisControleOperacao
|
||||
.EnviarDadosHeartbeat()
|
||||
.ConfigureAwait(false);
|
||||
},
|
||||
interval: 1000,
|
||||
timeout: 3000,
|
||||
scheduleMode:
|
||||
AsyncTaskTimerScheduleMode
|
||||
.FixedRateSkipMissed
|
||||
);
|
||||
|
||||
_tmrAtualizaRovers =
|
||||
new AsyncTaskTimerModel(
|
||||
"base.rovers.watchdog",
|
||||
async cancellationToken =>
|
||||
{
|
||||
await VariaveisControleOperacao
|
||||
.AtualizarListaRoversNaRede()
|
||||
.ConfigureAwait(false);
|
||||
},
|
||||
interval: 1000,
|
||||
timeout: 5000,
|
||||
scheduleMode:
|
||||
AsyncTaskTimerScheduleMode
|
||||
.FixedRateSkipMissed
|
||||
);
|
||||
|
||||
_tmrPosicaoBase =
|
||||
new AsyncTaskTimerModel(
|
||||
"base.gnss.position.publisher",
|
||||
async cancellationToken =>
|
||||
{
|
||||
await PublicarPosicaoBaseAsync(
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
},
|
||||
interval: intervaloPosicaoMs,
|
||||
timeout: 3000,
|
||||
scheduleMode:
|
||||
AsyncTaskTimerScheduleMode
|
||||
.FixedRateSkipMissed
|
||||
);
|
||||
|
||||
if (Mock && AddRover)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
VariaveisControleOperacao.AdicionarNovoRoverNaRede("01", "192.168.1.100");
|
||||
var r = VariaveisControleOperacao.RoversNaRede.FirstOrDefault();
|
||||
r.Modo = AgroBase.Models.Enums.ModoOperacao.MapaGPS;
|
||||
r.Descricao = "Operacao Mockada";
|
||||
r.DadosLeitura = new AgroBase.Models.Operacoes.OperacaoParametrosDadosModel()
|
||||
{
|
||||
StatusRover = StatusModulo.Operante,
|
||||
Operacao = new AgroBase.Models.Operacoes.OperacaoParametrosDadosOperacaoModel()
|
||||
_tmrMockUi =
|
||||
new AsyncTaskTimerModel(
|
||||
"base.mock.ui",
|
||||
async cancellationToken =>
|
||||
{
|
||||
Modo = AgroBase.Models.Enums.ModoOperacao.MapaGPS,
|
||||
Status = StatusOperacao.Parametrizando,
|
||||
await AtualizarMockUiAsync()
|
||||
.ConfigureAwait(false);
|
||||
},
|
||||
Atuador = new AgroBase.Models.Operacoes.OperacaoParametrosDadosAtuadorModel()
|
||||
{
|
||||
Bombas = new List<AgroBase.Models.Operacoes.OperacaoParametrosDadosBombaModel>()
|
||||
interval: 1000,
|
||||
timeout: 3000,
|
||||
scheduleMode:
|
||||
AsyncTaskTimerScheduleMode
|
||||
.FixedRateSkipMissed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void IniciarTimers()
|
||||
{
|
||||
_tmrHeartbeatBase?.Start();
|
||||
_tmrAtualizaRovers?.Start();
|
||||
_tmrPosicaoBase?.Start();
|
||||
_tmrMockUi?.Start();
|
||||
}
|
||||
|
||||
private async Task PublicarPosicaoBaseAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_closing ||
|
||||
Variaveis.GpsService == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GPSModel leitura =
|
||||
ObterSnapshotGpsBase();
|
||||
|
||||
if (leitura == null)
|
||||
return;
|
||||
|
||||
await VariaveisControleOperacao
|
||||
.EnviarDadosPosicaoAsync(
|
||||
leitura,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private GPSModel ObterSnapshotGpsBase()
|
||||
{
|
||||
try
|
||||
{
|
||||
GPSModel leitura =
|
||||
Variaveis
|
||||
.GpsService?
|
||||
.UltimaLeitura;
|
||||
|
||||
if (leitura == null)
|
||||
return null;
|
||||
|
||||
/*
|
||||
* GPSModel no projeto já é usado com Clone() em outros
|
||||
* serviços. O clone evita serializar um objeto mutável
|
||||
* enquanto a serial GNSS o atualiza.
|
||||
*/
|
||||
return leitura.Clone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Variaveis
|
||||
.GpsService?
|
||||
.UltimaLeitura;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EncerrarAsync()
|
||||
{
|
||||
await _lifecycleLock
|
||||
.WaitAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (!_initialized && !_closing)
|
||||
return;
|
||||
|
||||
await EncerrarInternoAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lifecycleLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EncerrarInternoAsync()
|
||||
{
|
||||
_closing = true;
|
||||
|
||||
try
|
||||
{
|
||||
_appCts?.Cancel();
|
||||
}
|
||||
catch { }
|
||||
|
||||
await PararTimerAsync(_tmrMockUi)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await PararTimerAsync(_tmrPosicaoBase)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await PararTimerAsync(_tmrAtualizaRovers)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await PararTimerAsync(_tmrHeartbeatBase)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_tmrMockUi = null;
|
||||
_tmrPosicaoBase = null;
|
||||
_tmrAtualizaRovers = null;
|
||||
_tmrHeartbeatBase = null;
|
||||
|
||||
if (_mockTask != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mockTask
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"Erro ao encerrar mock: " +
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
|
||||
_mockTask = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Variaveis
|
||||
.EncerrarMqttAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"Erro ao encerrar MQTT da base: " +
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Variaveis.StopUdpChannel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"Erro ao encerrar UDP da base: " +
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
|
||||
_appCts?.Dispose();
|
||||
_appCts = null;
|
||||
|
||||
_initialized = false;
|
||||
_closing = false;
|
||||
}
|
||||
|
||||
private static async Task PararTimerAsync(
|
||||
AsyncTaskTimerModel timer)
|
||||
{
|
||||
if (timer == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await timer.DisposeAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
"Erro ao parar timer " +
|
||||
timer.Id + ": " + ex.Message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConfigurarMockAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(
|
||||
1000,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
|
||||
await VariaveisControleOperacao
|
||||
.AdicionarNovoRoverNaRede(
|
||||
"01",
|
||||
"192.168.1.100"
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
OperacaoParametrosModel rover =
|
||||
VariaveisControleOperacao
|
||||
.GetRoversSnapshot()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (rover == null)
|
||||
return;
|
||||
|
||||
rover.Modo = ModoOperacao.MapaGPS;
|
||||
rover.Descricao = "Operacao Mockada";
|
||||
rover.Alive = true;
|
||||
rover.UltimoContato = DateTime.Now;
|
||||
|
||||
rover.DadosLeitura = CriarDadosMock();
|
||||
rover.Controle = CriarControleMock();
|
||||
|
||||
await AtualizarMockUiAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static OperacaoParametrosDadosModel CriarDadosMock()
|
||||
{
|
||||
return new OperacaoParametrosDadosModel
|
||||
{
|
||||
StatusRover = StatusModulo.Operante,
|
||||
|
||||
Operacao =
|
||||
new OperacaoParametrosDadosOperacaoModel
|
||||
{
|
||||
Modo = ModoOperacao.MapaGPS,
|
||||
Status = StatusOperacao.Parametrizando
|
||||
},
|
||||
|
||||
Atuador =
|
||||
new OperacaoParametrosDadosAtuadorModel
|
||||
{
|
||||
Bombas =
|
||||
new List<OperacaoParametrosDadosBombaModel>
|
||||
{
|
||||
new AgroBase.Models.Operacoes.OperacaoParametrosDadosBombaModel()
|
||||
new OperacaoParametrosDadosBombaModel
|
||||
{
|
||||
ComandoEstado = true,
|
||||
LeituraEstado = true,
|
||||
|
|
@ -67,9 +448,9 @@ namespace OperationControl.Models
|
|||
LeituraPotencia = 50,
|
||||
TempoAtuado = 3000,
|
||||
ComandoPressao = 85,
|
||||
LeituraPressao = 80,
|
||||
LeituraPressao = 80
|
||||
},
|
||||
new AgroBase.Models.Operacoes.OperacaoParametrosDadosBombaModel()
|
||||
new OperacaoParametrosDadosBombaModel
|
||||
{
|
||||
ID = "BMBAGT",
|
||||
ID_Num = 11,
|
||||
|
|
@ -78,185 +459,245 @@ namespace OperationControl.Models
|
|||
Inicializado = true,
|
||||
ComandoPotencia = 80,
|
||||
LeituraPotencia = 0,
|
||||
TempoAtuado = 100,
|
||||
TempoAtuado = 100
|
||||
}
|
||||
},
|
||||
Bicos = new List<AgroBase.Models.Operacoes.OperacaoParametrosDadosBicoModel>()
|
||||
|
||||
Bicos =
|
||||
new List<OperacaoParametrosDadosBicoModel>
|
||||
{
|
||||
new AgroBase.Models.Operacoes.OperacaoParametrosDadosBicoModel()
|
||||
{
|
||||
AnguloAbertura = 30,
|
||||
ComandoAngulo = 0,
|
||||
ComandoEstado = true,
|
||||
ID = "B01",
|
||||
ID_Num = 1,
|
||||
Inicializado = true,
|
||||
LeituraAngulo = 0,
|
||||
LeituraEstado = true,
|
||||
Posicao = 1,
|
||||
QtdAtuacoes = 5,
|
||||
TempoAtuado = 300,
|
||||
VazaoInstantaneaMLs = 15,
|
||||
VazaoMediaMLs = 30,
|
||||
VolumeVazadoML = 325
|
||||
},
|
||||
new AgroBase.Models.Operacoes.OperacaoParametrosDadosBicoModel()
|
||||
{
|
||||
AnguloAbertura = 30,
|
||||
ComandoAngulo = 0,
|
||||
ComandoEstado = true,
|
||||
ID = "B02",
|
||||
ID_Num = 2,
|
||||
Inicializado = true,
|
||||
LeituraAngulo = 0,
|
||||
LeituraEstado = false,
|
||||
Posicao = 2,
|
||||
QtdAtuacoes = 5,
|
||||
TempoAtuado = 300,
|
||||
VazaoInstantaneaMLs = 15,
|
||||
VazaoMediaMLs = 30,
|
||||
VolumeVazadoML = 325
|
||||
},
|
||||
new AgroBase.Models.Operacoes.OperacaoParametrosDadosBicoModel()
|
||||
{
|
||||
AnguloAbertura = 30,
|
||||
ComandoAngulo = 0,
|
||||
ComandoEstado = false,
|
||||
ID = "B03",
|
||||
ID_Num = 3,
|
||||
Inicializado = true,
|
||||
LeituraAngulo = 0,
|
||||
LeituraEstado = true,
|
||||
Posicao = 3,
|
||||
QtdAtuacoes = 5,
|
||||
TempoAtuado = 300,
|
||||
VazaoInstantaneaMLs = 15,
|
||||
VazaoMediaMLs = 30,
|
||||
VolumeVazadoML = 325
|
||||
},
|
||||
CriarBicoMock("B01", 1, true, true),
|
||||
CriarBicoMock("B02", 2, true, false),
|
||||
CriarBicoMock("B03", 3, false, true)
|
||||
},
|
||||
CapacidadeReservatorio = 60,
|
||||
DistanciaEstimadaRestanteMetros = 254.5,
|
||||
ErvasNoRadar = true,
|
||||
HerbicidaConsumidoMl = 1541.32,
|
||||
HerbicidaPorAtuacaoMl = 235.1,
|
||||
Iniciado = true,
|
||||
LPorMetro = 0.36,
|
||||
LPorMinuto = 1.2,
|
||||
MassaReservatorioKg = 25,
|
||||
PercentualErvasNoRadar = 3.4,
|
||||
PercentualErvasTerreno = 36.7,
|
||||
PercentualReservatorio = 47,
|
||||
PressaoLinhaPsi = 84.3,
|
||||
QtdCamerasSolo = 1,
|
||||
TempoEstimadoRestanteMinutos = 60,
|
||||
VazaoInstantaneaMLs = 19.3,
|
||||
VazaoMediaMLs = 15.8,
|
||||
VolumeReservatorioL = 26,
|
||||
VolumeVazaoMl = 1.39,
|
||||
},
|
||||
Controle = new AgroBase.Models.Operacoes.OperacaoParametrosDadosControleModel()
|
||||
{
|
||||
AlturaBarra = 50,
|
||||
Angulo = 0,
|
||||
PercentualVelocidadeSPKmh = 0,
|
||||
TipoMovimento = TipoMovimentoDirecional.RodasDianteiras,
|
||||
},
|
||||
Gnss = new AgroBase.Models.Operacoes.OperacaoParametrosDadosGNSSModel()
|
||||
{
|
||||
Latitude = -22.17254631649402,
|
||||
Longitude = -47.395203906184044,
|
||||
OrientacaoReal = 187
|
||||
},
|
||||
ModulosSaude = new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>()
|
||||
{
|
||||
new AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel()
|
||||
{
|
||||
modulo = T_Code.Gps,
|
||||
saude = 0,
|
||||
status = StatusModulo.Desconectado,
|
||||
},
|
||||
new AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel()
|
||||
{
|
||||
modulo = T_Code.Can,
|
||||
saude = 70,
|
||||
status = StatusModulo.Alerta,
|
||||
},
|
||||
}
|
||||
};
|
||||
r.Controle = new AgroBase.Models.Operacoes.OperacaoParametrosControleModel()
|
||||
|
||||
CapacidadeReservatorio = 60,
|
||||
DistanciaEstimadaRestanteMetros = 254.5,
|
||||
ErvasNoRadar = true,
|
||||
HerbicidaConsumidoMl = 1541.32,
|
||||
HerbicidaPorAtuacaoMl = 235.1,
|
||||
Iniciado = true,
|
||||
LPorMetro = 0.36,
|
||||
LPorMinuto = 1.2,
|
||||
MassaReservatorioKg = 25,
|
||||
PercentualErvasNoRadar = 3.4,
|
||||
PercentualErvasTerreno = 36.7,
|
||||
PercentualReservatorio = 47,
|
||||
PressaoLinhaPsi = 84.3,
|
||||
QtdCamerasSolo = 1,
|
||||
TempoEstimadoRestanteMinutos = 60,
|
||||
VazaoInstantaneaMLs = 19.3,
|
||||
VazaoMediaMLs = 15.8,
|
||||
VolumeReservatorioL = 26,
|
||||
VolumeVazaoMl = 1.39
|
||||
},
|
||||
|
||||
Controle =
|
||||
new OperacaoParametrosDadosControleModel
|
||||
{
|
||||
AtuAgitadorModo = ModoAgitadorCalda.Continuo,
|
||||
AtuAlturaAreaPulverizacao = 10,
|
||||
AtuDuracaoAtuacao = 200,
|
||||
AtuPercentualErvasBicoOff = 1,
|
||||
AtuPercentualErvasBicoOn = 2,
|
||||
AtuPercentualInicioPulverizacao = 70,
|
||||
AtuPressaoLinha = 80,
|
||||
DirAnguloMaximo = 30,
|
||||
DirecionalAutomatico = true,
|
||||
MovimentoAutomatico = true,
|
||||
PulverizadorAutomatico = true,
|
||||
DirTipoMovimento = TiposControladorDirecional.MPC,
|
||||
DirVelocidadeMovimento = 40,
|
||||
FrenagemAutomaticaAoParar = false,
|
||||
ImuParadaPorInclinacao = false,
|
||||
MovVelocidadeCErvasPercent = 20,
|
||||
MovVelocidadeSErvasPercent = 50,
|
||||
MpcHorizonte = 4,
|
||||
DirAuxilioSonar = false,
|
||||
MovAuxilioSonar = false,
|
||||
OakParadaPorObstaculo = false,
|
||||
RegistrarDadosPosProcessamento = false,
|
||||
AnteciparManobraCorredorM = 10
|
||||
};
|
||||
});
|
||||
}
|
||||
AlturaBarra = 50,
|
||||
Angulo = 0,
|
||||
PercentualVelocidadeSPKmh = 0,
|
||||
TipoMovimento =
|
||||
TipoMovimentoDirecional
|
||||
.RodasDianteiras
|
||||
},
|
||||
|
||||
Gnss =
|
||||
new OperacaoParametrosDadosGNSSModel
|
||||
{
|
||||
Latitude = -22.17254631649402,
|
||||
Longitude = -47.395203906184044,
|
||||
OrientacaoReal = 187
|
||||
},
|
||||
|
||||
ModulosSaude =
|
||||
new List<
|
||||
AgroBase.Models.Operadores
|
||||
.ManagerWorkerMessageResponseModulosPendentesModel>
|
||||
{
|
||||
new AgroBase.Models.Operadores
|
||||
.ManagerWorkerMessageResponseModulosPendentesModel
|
||||
{
|
||||
modulo = T_Code.Gps,
|
||||
saude = 0,
|
||||
status = StatusModulo.Desconectado
|
||||
},
|
||||
new AgroBase.Models.Operadores
|
||||
.ManagerWorkerMessageResponseModulosPendentesModel
|
||||
{
|
||||
modulo = T_Code.Can,
|
||||
saude = 70,
|
||||
status = StatusModulo.Alerta
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void tmrComunicacao_Elapsed()
|
||||
private static OperacaoParametrosDadosBicoModel
|
||||
CriarBicoMock(
|
||||
string id,
|
||||
int posicao,
|
||||
bool comando,
|
||||
bool leitura)
|
||||
{
|
||||
return new OperacaoParametrosDadosBicoModel
|
||||
{
|
||||
AnguloAbertura = 30,
|
||||
ComandoAngulo = 0,
|
||||
ComandoEstado = comando,
|
||||
ID = id,
|
||||
ID_Num = posicao,
|
||||
Inicializado = true,
|
||||
LeituraAngulo = 0,
|
||||
LeituraEstado = leitura,
|
||||
Posicao = posicao,
|
||||
QtdAtuacoes = 5,
|
||||
TempoAtuado = 300,
|
||||
VazaoInstantaneaMLs = 15,
|
||||
VazaoMediaMLs = 30,
|
||||
VolumeVazadoML = 325
|
||||
};
|
||||
}
|
||||
|
||||
private static OperacaoParametrosControleModel
|
||||
CriarControleMock()
|
||||
{
|
||||
return new OperacaoParametrosControleModel
|
||||
{
|
||||
AtuAgitadorModo = ModoAgitadorCalda.Continuo,
|
||||
AtuAlturaAreaPulverizacao = 10,
|
||||
AtuDuracaoAtuacao = 200,
|
||||
AtuPercentualErvasBicoOff = 1,
|
||||
AtuPercentualErvasBicoOn = 2,
|
||||
AtuPercentualInicioPulverizacao = 70,
|
||||
AtuPressaoLinha = 80,
|
||||
DirAnguloMaximo = 30,
|
||||
DirecionalAutomatico = true,
|
||||
MovimentoAutomatico = true,
|
||||
PulverizadorAutomatico = true,
|
||||
DirTipoMovimento =
|
||||
TiposControladorDirecional.MPC,
|
||||
DirVelocidadeMovimento = 40,
|
||||
FrenagemAutomaticaAoParar = false,
|
||||
ImuParadaPorInclinacao = false,
|
||||
MovVelocidadeCErvasPercent = 20,
|
||||
MovVelocidadeSErvasPercent = 50,
|
||||
MpcHorizonte = 4,
|
||||
DirAuxilioSonar = false,
|
||||
MovAuxilioSonar = false,
|
||||
OakParadaPorObstaculo = false,
|
||||
RegistrarDadosPosProcessamento = false,
|
||||
AnteciparManobraCorredorM = 10
|
||||
};
|
||||
}
|
||||
|
||||
private static Task AtualizarMockUiAsync()
|
||||
{
|
||||
System.Windows.Application
|
||||
.Current?
|
||||
.Dispatcher?
|
||||
.BeginInvoke(
|
||||
new Action(() =>
|
||||
{
|
||||
Variaveis.Dock?
|
||||
._vm?
|
||||
.AtualizarDadosTela(
|
||||
VariaveisControleOperacao
|
||||
.RoverEmFoco
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void AdicionarAlertaDock(
|
||||
string roverId,
|
||||
T_Code modulo,
|
||||
SeveridadeAlerta severidade,
|
||||
string mensagem,
|
||||
string modId = null)
|
||||
{
|
||||
System.Windows.Application
|
||||
.Current
|
||||
.Dispatcher
|
||||
.BeginInvoke(
|
||||
new Action(() =>
|
||||
{
|
||||
Dock?._vm?.AdicionarAlerta(
|
||||
roverId,
|
||||
modulo,
|
||||
severidade,
|
||||
mensagem,
|
||||
modId
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public void RemoverAlertaDock(
|
||||
string roverId,
|
||||
T_Code modulo,
|
||||
SeveridadeAlerta? severidade = null,
|
||||
string modId = null)
|
||||
{
|
||||
System.Windows.Application
|
||||
.Current
|
||||
.Dispatcher
|
||||
.BeginInvoke(
|
||||
new Action(() =>
|
||||
{
|
||||
Dock?._vm?.RemoverAlerta(
|
||||
roverId,
|
||||
modulo,
|
||||
severidade,
|
||||
modId
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private static void Forget(
|
||||
Task task,
|
||||
string context)
|
||||
{
|
||||
if (task == null)
|
||||
return;
|
||||
|
||||
_ = task.ContinueWith(
|
||||
completed =>
|
||||
{
|
||||
Exception ex =
|
||||
completed.Exception?
|
||||
.GetBaseException();
|
||||
|
||||
if (ex != null)
|
||||
{
|
||||
Log(
|
||||
context + ": " + ex.Message
|
||||
);
|
||||
}
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions
|
||||
.OnlyOnFaulted |
|
||||
TaskContinuationOptions
|
||||
.ExecuteSynchronously,
|
||||
TaskScheduler.Default
|
||||
);
|
||||
}
|
||||
|
||||
private static void Log(string message)
|
||||
{
|
||||
if (tmrComunicacaoTicking) return;
|
||||
tmrComunicacaoTicking = true;
|
||||
try
|
||||
{
|
||||
if (tmrComunicacaoTicks % (VariaveisEquipamento.TempoEntrePingsConexao / 1000) == 0 && Variaveis.GpsService != null)
|
||||
{
|
||||
VariaveisControleOperacao.EnviarDadosPosicao(Variaveis.GpsService.UltimaLeitura);
|
||||
}
|
||||
VariaveisControleOperacao.EnviarDadosHeartbeat();
|
||||
VariaveisControleOperacao.AtualizarListaRoversNaRede();
|
||||
|
||||
if (Mock && AddRover)
|
||||
{
|
||||
System.Windows.Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
|
||||
{
|
||||
Variaveis.Dock?._vm?.AtualizarDadosTela(VariaveisControleOperacao.RoverEmFoco);
|
||||
}));
|
||||
}
|
||||
Variaveis.MostrarLog(message);
|
||||
}
|
||||
finally
|
||||
catch
|
||||
{
|
||||
tmrComunicacaoTicks++;
|
||||
tmrComunicacaoTicking = false;
|
||||
System.Diagnostics.Debug.WriteLine(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void AdicionarAlertaDock(string roverId, T_Code modulo, SeveridadeAlerta severidade, string mensagem, string modId = null)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
Dock?._vm?.AdicionarAlerta(roverId, modulo, severidade, mensagem, modId);
|
||||
}));
|
||||
}
|
||||
|
||||
public void RemoverAlertaDock(string roverId, T_Code modulo, SeveridadeAlerta? severidade = null, string modId = null)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
Dock?._vm?.RemoverAlerta(roverId, modulo, severidade, modId);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue