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 { /// /// 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. /// 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; } /// /// Compatibilidade com o contrato antigo. /// Para startup determinístico, prefira: /// await InicializarAsync(); /// 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 { 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 { 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); } } } }