agrobot_base/AgroBase/OperationControl/Models/AppShell.cs

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);
}
}
}
}