704 lines
22 KiB
C#
704 lines
22 KiB
C#
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 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()
|
|
{
|
|
Forget(
|
|
InicializarAsync(),
|
|
"Inicializar AppShell"
|
|
);
|
|
}
|
|
|
|
public async Task InicializarAsync(
|
|
CancellationToken cancellationToken =
|
|
default(CancellationToken))
|
|
{
|
|
await _lifecycleLock
|
|
.WaitAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
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()
|
|
{
|
|
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)
|
|
{
|
|
_tmrMockUi =
|
|
new AsyncTaskTimerModel(
|
|
"base.mock.ui",
|
|
async cancellationToken =>
|
|
{
|
|
await AtualizarMockUiAsync()
|
|
.ConfigureAwait(false);
|
|
},
|
|
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 OperacaoParametrosDadosBombaModel
|
|
{
|
|
ComandoEstado = true,
|
|
LeituraEstado = true,
|
|
ID = "BMBLN",
|
|
ID_Num = 10,
|
|
Inicializado = true,
|
|
ComandoPotencia = 50,
|
|
LeituraPotencia = 50,
|
|
TempoAtuado = 3000,
|
|
ComandoPressao = 85,
|
|
LeituraPressao = 80
|
|
},
|
|
new OperacaoParametrosDadosBombaModel
|
|
{
|
|
ID = "BMBAGT",
|
|
ID_Num = 11,
|
|
ComandoEstado = true,
|
|
LeituraEstado = false,
|
|
Inicializado = true,
|
|
ComandoPotencia = 80,
|
|
LeituraPotencia = 0,
|
|
TempoAtuado = 100
|
|
}
|
|
},
|
|
|
|
Bicos =
|
|
new List<OperacaoParametrosDadosBicoModel>
|
|
{
|
|
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 OperacaoParametrosDadosControleModel
|
|
{
|
|
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 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)
|
|
{
|
|
try
|
|
{
|
|
Variaveis.MostrarLog(message);
|
|
}
|
|
catch
|
|
{
|
|
System.Diagnostics.Debug.WriteLine(message);
|
|
}
|
|
}
|
|
}
|
|
}
|