From 1e7b72542205b1d5b8b0a96ddd09d4309b21b376 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Fri, 4 Sep 2026 18:43:34 -0300 Subject: [PATCH] otimizado o simulador --- .../Forms/Simuladores/frmSimulacaoMapaGPS.cs | 2152 +++++++++++++---- .../Models/Operacoes/OperacaoModel.cs | 2 +- AgroBase/AgroBase/Services/GPSService.cs | 286 ++- 3 files changed, 2013 insertions(+), 427 deletions(-) diff --git a/AgroBase/AgroBase/Forms/Simuladores/frmSimulacaoMapaGPS.cs b/AgroBase/AgroBase/Forms/Simuladores/frmSimulacaoMapaGPS.cs index fcddcc1ab..785033b10 100644 --- a/AgroBase/AgroBase/Forms/Simuladores/frmSimulacaoMapaGPS.cs +++ b/AgroBase/AgroBase/Forms/Simuladores/frmSimulacaoMapaGPS.cs @@ -1,597 +1,1702 @@ -using AgroBase.Models; +using AgroBase.Models; using AgroBase.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.Threading; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using static AgroBase.Models.Enums; -using System.Diagnostics; -using Emgu.CV; namespace AgroBase.Forms { + /// + /// Simulador de operação por mapa/GNSS. + /// + /// A simulação física nunca depende da thread da interface: + /// - o AsyncTaskTimerModel executa em FixedRateSkipMissed, sem uiControl; + /// - o passo usa duração nominal limitada, sem salto após travamento visual; + /// - a interface consome apenas o snapshot mais recente em baixa frequência; + /// - o mapa recebe trajetórias decimadas exclusivamente para visualização; + /// - nunca existem dois passos de simulação simultâneos. + /// + /// Os nomes dos handlers e as APIs públicas usadas pelo Designer foram + /// preservados para permitir substituição direta do code-behind atual. + /// public partial class frmSimulacaoMapaGPS : Form { - AsyncTaskTimerModel tmrLeitura; - MapaDinamicoModel MapaDinamico; - bool parar = false; - double tempoGps = (1000.0 / GPSService.TaxaAmostragemHz); - int ticksControleMax = 0; - int ticksControle = 0; - List passosOperacao = new List(); - int idxPassoOperacao = 0; - double velocidadeCarroMs = 0.0; + #region Configuração - DateTime UltimaAtualizacaoGps = DateTime.MinValue; + private const int IntervaloMinimoGpsMs = 20; + private const int IntervaloMaximoGpsMs = 10_000; + private const int IntervaloTelaMs = 250; // 4 FPS. A física continua independente. + private const int IntervaloTrajetoriaWebMs = 1_000; - Queue historicoAngulosControle = new Queue(); - GPSModel posicaoAtual = new GPSModel(); + private const int MaxPontosTrajetoriaVisual = 800; + private const int MaxPontosCorredorVisual = 600; + private const int MaxPontosPorRuaVisual = 350; + private const int MaxPontosTrilhaVisual = 1_500; - List simulacao = new List(); + #endregion + #region Serviços e sincronização + private AsyncTaskTimerModel tmrLeitura; + private System.Windows.Forms.Timer tmrTela; + private MapaDinamicoModel MapaDinamico; + + private readonly SemaphoreSlim _passoSimulacaoGate = + new SemaphoreSlim(1, 1); + + private readonly object _estadoSimulacaoLock = new object(); + private readonly Queue historicoAngulosControle = + new Queue(); + private readonly Queue _trilhaVisual = + new Queue(); + + private int _encerrando; private int _atualizacaoTelaPendente; - private int _tickEmExecucao; + private int _passoManualEmExecucao; + private int _mapaWebEmExecucao; + private int _forcarTrajetoriaWeb = 1; + private int _publicacaoMapaGpsConfigurada; + private bool _publicacaoMapaGpsAnterior = true; - private readonly Stopwatch _cronometroTela = Stopwatch.StartNew(); + #endregion - private const int IntervaloTelaMs = 100; // 10 FPS para a UI + #region Estado da simulação + private bool parar; + + private double tempoGps = + 1000.0 / Math.Max(1, GPSService.TaxaAmostragemHz); + + private int _tempoGpsConfiguradoMs; + private int _automatico; + private int _delayComandoPassos; + private int _delayPosicaoPassos; + + private int ticksControleMax; + private int ticksControle; + + private List passosOperacao = new List(); + private int idxPassoOperacao; + + private double velocidadeCarroMs; + private double _ultimoAnguloControleAplicado; + private GPSModel posicaoAtual = new GPSModel(); + + // Mantida para compatibilidade e para os dados apresentados na tela. + private List simulacao = + new List(); + + private readonly Stopwatch _cronometroTrajetoriaWeb = + Stopwatch.StartNew(); + + private object _ruasFonteCache; + private int _ruasFonteQuantidadeRuas = -1; + private List> _ruasVisualCache = + new List>(); + + #endregion public frmSimulacaoMapaGPS() { InitializeComponent(); - // Ativa o double buffering - this.DoubleBuffered = true; - this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); - this.SetStyle(ControlStyles.UserPaint, true); - this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + DoubleBuffered = true; + SetStyle(ControlStyles.AllPaintingInWmPaint, true); + SetStyle(ControlStyles.UserPaint, true); + SetStyle(ControlStyles.OptimizedDoubleBuffer, true); MapaDinamico = new MapaDinamicoModel(this, pnlZoomMapa); } + #region Ciclo de vida + private void frmSimulacaoMapaGPS_Load(object sender, EventArgs e) { cmbTipoControleDirecional.Items.Clear(); - cmbTipoControleDirecional.Items.AddRange(Enum.GetNames(typeof(Enums.TiposControladorDirecional))); - cmbTipoControleDirecional.SelectedIndex = (int)Variaveis.OperacaoEmAndamento.Parametros.Controle.DirTipoMovimento; + cmbTipoControleDirecional.Items.AddRange( + Enum.GetNames(typeof(Enums.TiposControladorDirecional)) + ); + cmbTipoControleDirecional.SelectedIndex = + (int)(Variaveis.OperacaoEmAndamento.Parametros.Controle?.DirTipoMovimento ?? Enums.TiposControladorDirecional.MPC); cmbStatusCorredor.Items.Clear(); - cmbStatusCorredor.Items.AddRange(Enum.GetNames(typeof(Enums.StatusCarroMapa))); + cmbStatusCorredor.Items.AddRange( + Enum.GetNames(typeof(Enums.StatusCarroMapa)) + ); cmbStatusCorredor.SelectedIndex = (int)StatusCarroMapa.Parado; Variaveis.OperacaoEmAndamento.ReiniciarSimulacao(); - tmrLeitura = new AsyncTaskTimerModel("tmrLeitura", tmrLeitura_Tick, (int)tempoGps, this); - tmrLeitura.Start(); + int intervaloInicial = LimitarInteiro( + (int)Math.Round(tempoGps), + IntervaloMinimoGpsMs, + IntervaloMaximoGpsMs + ); - txtTempoGPS.Text = tempoGps.ToString(); + txtTempoGPS.Text = intervaloInicial.ToString(CultureInfo.CurrentCulture); + Interlocked.Exchange(ref _tempoGpsConfiguradoMs, intervaloInicial); + + MapasModel mapaAnterior = Variaveis.OperacaoEmAndamento.Mapa; + if (mapaAnterior != null) + { + try + { + mapaAnterior.RuasSelecionadasAlteradas -= + Mapa_RuasSelecionadasAlteradas; + } + catch + { + } + } Variaveis.OperacaoEmAndamento.Mapa = new MapasModel() { pnlMapa = pnlMapa, }; - Variaveis.OperacaoEmAndamento.Mapa.RuasSelecionadasAlteradas += Mapa_RuasSelecionadasAlteradas; + Variaveis.OperacaoEmAndamento.Mapa.RuasSelecionadasAlteradas += + Mapa_RuasSelecionadasAlteradas; + + // Durante a simulação, somente o timer visual deste Form publica + // no WebView. Isso impede publicação duplicada no GPSService e, + // principalmente, acesso ao WebView pela thread da física. + _publicacaoMapaGpsAnterior = + GPSService.PublicacaoMapaAutomaticaHabilitada; + GPSService.PublicacaoMapaAutomaticaHabilitada = false; + Interlocked.Exchange(ref _publicacaoMapaGpsConfigurada, 1); + + AtualizarConfiguracaoSimulacaoDaTela(); + + // Sem uiControl: o callback nunca é encaminhado para a UI. + tmrLeitura = new AsyncTaskTimerModel( + "tmrLeituraSimulacao_" + GetHashCode(), + tmrLeitura_Tick, + intervaloInicial, + uiControl: null, + timeout: Math.Max(5_000, intervaloInicial * 10), + scheduleMode: AsyncTaskTimerScheduleMode.FixedRateSkipMissed, + runImmediately: false + ); + tmrLeitura.Start(); + + // Timer exclusivamente visual. Se a UI atrasar, ele próprio atrasa, + // mas nunca interfere no relógio da simulação. + tmrTela = new System.Windows.Forms.Timer(); + tmrTela.Interval = IntervaloTelaMs; + tmrTela.Tick += tmrTela_Tick; + tmrTela.Start(); + + AtualizarDadosTela(); } - private void Mapa_RuasSelecionadasAlteradas(object sender, EventArgs e) + private void frmSimulacaoMapaGPS_FormClosing( + object sender, + FormClosingEventArgs e) { - //Variaveis.OperacaoEmAndamento.Parametros.RuasPercorrer = mapa.RuasPercorrer.ToList(); + EncerrarRecursos(); + + var operacao = Variaveis.OperacaoEmAndamento; + if (operacao?.Sensoriamento?.Operacao != null) + operacao.Sensoriamento.Operacao.OperacaoIniciada = false; } - private void frmSimulacaoMapaGPS_FormClosing(object sender, FormClosingEventArgs e) + private void EncerrarRecursos() { - tmrLeitura?.Dispose(); - MapaDinamico?.Dispose(); - Variaveis.OperacaoEmAndamento.Sensoriamento.Operacao.OperacaoIniciada = false; - } - - private async Task tmrLeitura_Tick() - { - if (Interlocked.Exchange(ref _tickEmExecucao, 1) == 1) + if (Interlocked.Exchange(ref _encerrando, 1) == 1) return; + if (Interlocked.Exchange( + ref _publicacaoMapaGpsConfigurada, + 0) == 1) + { + GPSService.PublicacaoMapaAutomaticaHabilitada = + _publicacaoMapaGpsAnterior; + } + try { - int tempo = 0; - bool automatico = false; - - // Esses componentes pertencem à UI. - if (!IsDisposed && IsHandleCreated) + if (tmrTela != null) { - Invoke(new Action(() => - { - int.TryParse(txtTempoGPS.Text, out tempo); - automatico = chbAutomatico.Checked; - })); + tmrTela.Stop(); + tmrTela.Tick -= tmrTela_Tick; + tmrTela.Dispose(); + tmrTela = null; + } + } + catch + { + } + + try + { + tmrLeitura?.Dispose(); + tmrLeitura = null; + } + catch + { + } + + try + { + var mapa = Variaveis.OperacaoEmAndamento?.Mapa; + if (mapa != null) + { + mapa.RuasSelecionadasAlteradas -= + Mapa_RuasSelecionadasAlteradas; + mapa.Dispose(); + } + } + catch + { + } + + try + { + MapaDinamico?.Dispose(); + MapaDinamico = null; + } + catch + { + } + } + + #endregion + + #region Relógio da simulação + + private async Task tmrLeitura_Tick(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested || + Volatile.Read(ref _encerrando) == 1) + { + return; + } + + if (Volatile.Read(ref _automatico) == 0) + return; + + await ExecutarPassoSimulacaoAsync( + cancellationToken, + aguardarGate: false + ).ConfigureAwait(false); + } + + private async Task ExecutarPassoSimulacaoAsync( + CancellationToken cancellationToken, + bool aguardarGate) + { + bool entrou = false; + + try + { + if (aguardarGate) + { + await _passoSimulacaoGate + .WaitAsync(cancellationToken) + .ConfigureAwait(false); + entrou = true; + } + else + { + entrou = await _passoSimulacaoGate + .WaitAsync(0, cancellationToken) + .ConfigureAwait(false); + + if (!entrou) + return false; } - if (tempo > 0) - tempoGps = tempo; + cancellationToken.ThrowIfCancellationRequested(); - if (automatico) - { - // Temporariamente mantém o passo na UI porque o método ainda - // acessa vários TextBox diretamente. - Invoke(new Action(() => - { - btnAcrescentarGPS_Click( - btnAcrescentarGPS, - EventArgs.Empty - ); - })); - } - - if (_cronometroTela.ElapsedMilliseconds >= IntervaloTelaMs) - { - SolicitarAtualizacaoTela(); - _cronometroTela.Restart(); - } - - // Não reduza para 1 ms quando estiver atrasado. - tmrLeitura.SetInterval(Math.Max((int)tempoGps, 10)); + ExecutarPassoSimulacaoCore(cancellationToken); + return true; + } + catch (OperationCanceledException) + { + return false; + } + catch (Exception ex) + { + Variaveis.MostrarLog( + "[Simulacao] Falha no passo de simulação: " + ex + ); + return false; } finally { - Interlocked.Exchange(ref _tickEmExecucao, 0); + if (entrou) + _passoSimulacaoGate.Release(); + } + } + + private void ExecutarPassoSimulacaoCore( + CancellationToken cancellationToken) + { + var operacao = Variaveis.OperacaoEmAndamento; + + if (operacao == null || + operacao.Controle == null || + operacao.Sensoriamento == null) + { + return; } - await Task.CompletedTask; - } - private void SolicitarAtualizacaoTela() - { - if (IsDisposed || !IsHandleCreated) - return; + cancellationToken.ThrowIfCancellationRequested(); - if (Interlocked.Exchange(ref _atualizacaoTelaPendente, 1) == 1) - return; + int intervaloMs = LimitarInteiro( + Volatile.Read(ref _tempoGpsConfiguradoMs), + IntervaloMinimoGpsMs, + IntervaloMaximoGpsMs + ); - BeginInvoke(new Action(() => + // Passo nominal e limitado. Um atraso de UI nunca causa teleporte. + double tempoPassadoSegundos = intervaloMs / 1000.0; + + double velocidadeCalculada = + FuncoesMatematicas.CalculaVelocidadeMsPercentual( + operacao.SimulacaoRpmControle + ); + + var controleMovimento = operacao.Controle.TiposControle? + .FirstOrDefault(x => x.Tipo == T_Code.Mov); + + if (controleMovimento != null && + controleMovimento.DirecaoAtual == Direcao.Tras) { - try + velocidadeCalculada *= -1.0; + } + + if (operacao.DispMvd?.Dados != null) + operacao.DispMvd.Dados.VelocidadeMedia = velocidadeCalculada; + + double velocidadeFisica = + operacao.Sensoriamento.Operacao.StatusOperacaoAtual == + StatusOperacao.EmAndamento + ? velocidadeCalculada + : 0.0; + + GPSModel novaPosicao = ObterPassoOperacaoGravado(operacao); + double anguloControle = ObterUltimoAnguloControle(); + + if (novaPosicao == null) + { + bool atualizarAnguloControle = + ticksControle >= ticksControleMax; + + if (atualizarAnguloControle) + ticksControle = 0; + else + ticksControle++; + + if (atualizarAnguloControle) { - AtualizarDadosTela(); - } - catch (Exception ex) - { - Variaveis.MostrarLog( - "[Simulacao] Erro ao atualizar tela: " + ex + anguloControle = CalcularAnguloControleAtrasado( + Volatile.Read(ref _delayComandoPassos) ); } - finally + + GPSModel gps = GPSService.GetSnapshot(); + + double anguloAtual = + ValorFinito(gps?.AnguloCarroDefinido ?? double.NaN) + ? gps.AnguloCarroDefinido + : ValorFinito(gps?.OrientacaoReal ?? double.NaN) + ? gps.OrientacaoReal + : 0.0; + + TipoMovimentoDirecional tipoMovimento = + operacao.Controle.TipoMovimento; + + novaPosicao = VariaveisEquipamento.SimularNovaPosicao( + tipoMovimento, + anguloControle, + velocidadeFisica, + tempoPassadoSegundos, + anguloAtual, + gps + ); + } + + if (novaPosicao == null) + return; + + PrepararLeituraSimulada( + novaPosicao, + velocidadeFisica, + intervaloMs + ); + + cancellationToken.ThrowIfCancellationRequested(); + + AtualizarPosicaoGPS( + novaPosicao, + Volatile.Read(ref _delayPosicaoPassos) + ); + + GPSModel posicaoCorrigida = GPSService.GetSnapshot(); + + lock (_estadoSimulacaoLock) + { + posicaoAtual = novaPosicao.Clone(); + velocidadeCarroMs = velocidadeCalculada; + _ultimoAnguloControleAplicado = anguloControle; + + if (posicaoCorrigida != null) { - Interlocked.Exchange(ref _atualizacaoTelaPendente, 0); + _trilhaVisual.Enqueue(posicaoCorrigida.Clone()); + + while (_trilhaVisual.Count > MaxPontosTrilhaVisual) + _trilhaVisual.Dequeue(); } - })); + } + + // Sensoriamento.AtualizarDados() não é chamado aqui. + // O tmrLeituras da OperacaoModel continua sendo o único proprietário + // do snapshot/contexto Redis, igual ao fluxo real do produto. } + + private GPSModel ObterPassoOperacaoGravado(OperacaoModel operacao) + { + if (operacao?.Simulando != true) + return null; + + lock (_estadoSimulacaoLock) + { + if (passosOperacao == null || passosOperacao.Count == 0) + return null; + + int indice = Math.Min( + Math.Max(0, idxPassoOperacao), + passosOperacao.Count - 1 + ); + + GPSModel leitura = passosOperacao[indice]?.Clone(); + + if (idxPassoOperacao < passosOperacao.Count - 1) + idxPassoOperacao++; + + return leitura; + } + } + + private void PrepararLeituraSimulada( + GPSModel leitura, + double velocidade, + int intervaloMs) + { + if (leitura == null) + return; + + DateTime agora = DateTime.Now; + double timestampMonotonico = + Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency; + + leitura.Momento = agora; + leitura.DataHora = agora; + leitura.UltimoComandoRespondido = DateTime.UtcNow; + leitura.Velocidade = velocidade; + + if (!CoordenadaGeograficaValida( + leitura.LatitudeAnt, + leitura.LongitudeAnt) && + CoordenadaGeograficaValida( + leitura.Latitude, + leitura.Longitude)) + { + leitura.LatitudeAnt = leitura.Latitude; + leitura.LongitudeAnt = leitura.Longitude; + } + + if (leitura.TimestampPos != null) + leitura.TimestampPos.valor = timestampMonotonico; + + if (leitura.TimestampOri != null) + leitura.TimestampOri.valor = timestampMonotonico; + + // O campo Distancia normalmente já vem preenchido por + // SimularNovaPosicao. Para trajetórias importadas, preservamos o valor. + if (!ValorFinito(leitura.Distancia) || leitura.Distancia < 0) + leitura.Distancia = Math.Abs(velocidade) * intervaloMs / 1000.0; + } + + private void AtualizarPosicaoGPS( + GPSModel leitura, + int delayPosicaoPassos) + { + if (leitura == null) + return; + + // A escrita e o processamento completo ocorrem sob o mesmo lock + // interno usado pelas sentenças NMEA reais. O Form não altera + // mais os campos estáticos do GPSService de forma fragmentada. + GPSService.AtualizarLeituraSimulada( + leitura, + Math.Max(0, delayPosicaoPassos) + ); + } + + #endregion + + #region Atualização da interface + + private void tmrTela_Tick(object sender, EventArgs e) + { + if (Volatile.Read(ref _encerrando) == 1) + return; + + AtualizarConfiguracaoSimulacaoDaTela(); + AtualizarDadosTela(); + } + + private void AtualizarConfiguracaoSimulacaoDaTela() + { + if (IsDisposed || Disposing) + return; + + int intervaloAtual = Volatile.Read(ref _tempoGpsConfiguradoMs); + int novoIntervalo; + + if (!int.TryParse( + txtTempoGPS.Text, + NumberStyles.Integer, + CultureInfo.CurrentCulture, + out novoIntervalo)) + { + novoIntervalo = intervaloAtual > 0 + ? intervaloAtual + : (int)Math.Round(tempoGps); + } + + novoIntervalo = LimitarInteiro( + novoIntervalo, + IntervaloMinimoGpsMs, + IntervaloMaximoGpsMs + ); + + if (novoIntervalo != intervaloAtual) + { + Interlocked.Exchange( + ref _tempoGpsConfiguradoMs, + novoIntervalo + ); + tempoGps = novoIntervalo; + + try + { + tmrLeitura?.SetInterval(novoIntervalo); + tmrLeitura?.SetTimeout( + Math.Max(5_000, novoIntervalo * 10) + ); + } + catch (ObjectDisposedException) + { + } + } + + Interlocked.Exchange( + ref _automatico, + chbAutomatico.Checked ? 1 : 0 + ); + + int delayComando; + if (!int.TryParse(txtDelayComando.Text, out delayComando)) + delayComando = 0; + + int delayPosicao; + if (!int.TryParse(txtDelayPosicao.Text, out delayPosicao)) + delayPosicao = 0; + + Interlocked.Exchange( + ref _delayComandoPassos, + LimitarInteiro(delayComando, 0, 100) + ); + Interlocked.Exchange( + ref _delayPosicaoPassos, + LimitarInteiro(delayPosicao, 0, 100) + ); + } + + private void SolicitarAtualizacaoTela() + { + if (Volatile.Read(ref _encerrando) == 1 || + IsDisposed || + !IsHandleCreated) + { + return; + } + + if (Interlocked.Exchange( + ref _atualizacaoTelaPendente, + 1) == 1) + { + return; + } + + try + { + BeginInvoke(new Action(delegate + { + try + { + if (Volatile.Read(ref _encerrando) == 0) + { + AtualizarConfiguracaoSimulacaoDaTela(); + AtualizarDadosTela(); + } + } + catch (Exception ex) + { + Variaveis.MostrarLog( + "[Simulacao] Erro ao atualizar tela: " + ex + ); + } + finally + { + Interlocked.Exchange( + ref _atualizacaoTelaPendente, + 0 + ); + } + })); + } + catch (ObjectDisposedException) + { + Interlocked.Exchange(ref _atualizacaoTelaPendente, 0); + } + catch (InvalidOperationException) + { + Interlocked.Exchange(ref _atualizacaoTelaPendente, 0); + } + } + private void AtualizarDadosTela() { - var _Controle = Variaveis.OperacaoEmAndamento.Controle; - var _Sensoriamento = Variaveis.OperacaoEmAndamento.Sensoriamento; - var _Trajetoria = Variaveis.OperacaoEmAndamento.Trajetoria; + var operacao = Variaveis.OperacaoEmAndamento; + var controle = operacao?.Controle; + var sensoriamento = operacao?.Sensoriamento; + var trajetoria = operacao?.Trajetoria; - if (_Trajetoria == null) + if (operacao == null || + controle == null || + sensoriamento == null || + sensoriamento.Operacao == null || + trajetoria == null || + sensoriamento.Trajetoria == null) { return; } - lblStatusOperacao.Text = $"Operação: {_Sensoriamento.Operacao.StatusOperacaoAtual}"; - if (_Sensoriamento.Operacao.StatusOperacaoAtual == StatusOperacao.Aguardando) lblStatusOperacao.Text += $" ({_Sensoriamento.Operacao.TempoAguardandoSeg:0})"; - lblTipoMovimento.Text = (_Controle?.TipoMovimento ?? TipoMovimentoDirecional.Diagnostico).ToString(); + // Antes de a operação começar, latitude, longitude e ângulo + // pertencem ao operador e nunca são sobrescritos pelo timer visual. + // Durante a operação, eles voltam a funcionar como telemetria. + if (sensoriamento.Operacao.OperacaoIniciada) + AtualizarCamposPosicaoSimuladaNaTela(); - if (_Trajetoria == null) + var dadosTrajetoria = sensoriamento.Trajetoria; + var corredor = dadosTrajetoria.CorredorAtual; + var proximoPonto = dadosTrajetoria.ProximoPonto; + var pontoMaisProximo = dadosTrajetoria.PontoMaisProximo; + + lblStatusOperacao.Text = + "Operação: " + sensoriamento.Operacao.StatusOperacaoAtual; + + if (sensoriamento.Operacao.StatusOperacaoAtual == + StatusOperacao.Aguardando) { - //Variaveis.OperacaoEmAndamento.Mapa.AtualizarRuasSelecionadas(); - return; + lblStatusOperacao.Text += + $" ({sensoriamento.Operacao.TempoAguardandoSeg:0})"; } - if (_Sensoriamento.Trajetoria == null) + lblTipoMovimento.Text = + (controle.TipoMovimento).ToString(); + + string statusVisual = "-"; + try + { + statusVisual = sensoriamento + .OperadorVisual + .Analises + .segmentacao + .status_corredor + .ToString(); + } + catch { - return; } - var h = GPSUtils.DistanciaDoTrecho(simulacao.Select(x => new GPSModel() { Latitude = x.latitude, Longitude = x.longitude, OrientacaoReal = x.orientacao }).ToList()); + lblStatus.Text = + $"Carro: {dadosTrajetoria.StatusCarro} ({statusVisual})"; + lblDirecao.Text = + $"Direção: {dadosTrajetoria.DirecaoCaminho}"; + lblDistanciaOperacao.Text = + $"Distância Operação: {dadosTrajetoria.DistanciaPercorrida:F2} m de {dadosTrajetoria.DistanciaTotal:F2} m"; + lblDistanciaFinal.Text = + $"Distância Restante: {dadosTrajetoria.DistanciaRestante:F2} m"; - lblStatus.Text = $"Carro: {_Sensoriamento.Trajetoria.StatusCarro} ({_Sensoriamento.OperadorVisual.Analises.segmentacao.status_corredor})"; - lblDirecao.Text = $"Direção: {_Sensoriamento.Trajetoria.DirecaoCaminho}"; - lblDistanciaOperacao.Text = $"Distância Operação: {_Sensoriamento.Trajetoria.DistanciaPercorrida:F2} m de {_Sensoriamento.Trajetoria.DistanciaTotal:F2} m"; - lblDistanciaFinal.Text = $"Distância Restante: {_Sensoriamento.Trajetoria.DistanciaRestante:F2} m"; - //lblDistanciaLateral.Text = $"Esq: {_Sensoriamento.Trajetoria.DistanciaEsquerda:F2} m - Dir: {_Sensoriamento.Trajetoria.DistanciaDireita:F2} m {Variaveis.OperacaoEmAndamento.Controle.ErroLateral:F2} m"; - lblDistanciaLateral.Text = $"h: {h:F2} m, p: {_Controle.SimulacaoMPC.Count}, s: {_Controle.DebugCustoMpc.Count}"; - lblRua.Text = $"Corredor: " + (_Sensoriamento.Trajetoria.CorredorAtual.Dentro ? "Dentro" : "Fora"); - lblMargem.Text = $"Margem: " + (_Sensoriamento.Trajetoria.NaMargemDoCorredor ? "Sim" : "Não"); - lblProximoPonto.Text = $"Próximo Ponto: {_Sensoriamento.Trajetoria.ProximoPonto.idxPonto}"; - lblAproximando.Text = (_Sensoriamento.Trajetoria.ProximoPonto.Aproximando ? "Aproximando" : "Afastando"); - lblDistProx.Text = $"Distância Próximo Ponto: {_Sensoriamento.Trajetoria.ProximoPonto.DistanciaTrajeto:F2} m"; - lblDistAnt.Text = $"Distância Ponto Anterior: {_Sensoriamento.Trajetoria.PontoMaisProximo.DistanciaTrajeto:F2} m"; - lblTempoRestante.Text = $"Tempo Restante: {_Sensoriamento.Trajetoria.TempoEstimadoRestante}"; - lblTempoOperacao.Text = $"Tempo: " + _Sensoriamento.Operacao.TempoDecorrido.ToString("HH:mm:ss") + $" de {_Sensoriamento.Trajetoria.TempoEstimadoOperacao}"; - lblPercentualRua.Text = $"Rua: {_Sensoriamento.Trajetoria.CorredorAtual.Progresso:F2}%"; - lblPercentualOperacao.Text = $"Operação: {_Sensoriamento.Trajetoria.ProgressoTrajeto:F2}%"; + List simulacaoAtual = + CopiarListaSegura(controle.SimulacaoMPC); + simulacao = simulacaoAtual; + double distanciaHorizonte = + CalcularDistanciaSimulacao(simulacaoAtual); + + int qtdDebug = controle.DebugCustoMpc?.Count ?? 0; + AsyncTaskTimerMetrics metricas = tmrLeitura?.GetMetrics(); + + lblDistanciaLateral.Text = + $"h: {distanciaHorizonte:F2} m, " + + $"p: {simulacaoAtual.Count}, s: {qtdDebug}, " + + $"tick: {(metricas?.LastExecutionDurationMs ?? 0):F0} ms, " + + $"atrasos: {metricas?.Overruns ?? 0}"; + + lblRua.Text = + "Corredor: " + + ((corredor?.Dentro ?? false) ? "Dentro" : "Fora"); + lblMargem.Text = + "Margem: " + + (dadosTrajetoria.NaMargemDoCorredor ? "Sim" : "Não"); + lblProximoPonto.Text = + $"Próximo Ponto: {proximoPonto?.idxPonto ?? 0}"; + lblAproximando.Text = + (proximoPonto?.Aproximando ?? false) + ? "Aproximando" + : "Afastando"; + lblDistProx.Text = + $"Distância Próximo Ponto: {(proximoPonto?.DistanciaTrajeto ?? 0):F2} m"; + lblDistAnt.Text = + $"Distância Ponto Anterior: {(pontoMaisProximo?.DistanciaTrajeto ?? 0):F2} m"; + lblTempoRestante.Text = + $"Tempo Restante: {dadosTrajetoria.TempoEstimadoRestante}"; + lblTempoOperacao.Text = + "Tempo: " + + sensoriamento.Operacao.TempoDecorrido.ToString("HH:mm:ss") + + $" de {dadosTrajetoria.TempoEstimadoOperacao}"; + lblPercentualRua.Text = + $"Rua: {(corredor?.Progresso ?? 0):F2}%"; + lblPercentualOperacao.Text = + $"Operação: {dadosTrajetoria.ProgressoTrajeto:F2}%"; pnlOrientacaoTrajeto.Invalidate(); pnlAnguloControle.Invalidate(); pnlOrientacaoCarro.Invalidate(); - if (_Controle.SimulacaoMPC != null) + AtualizarMapasVisuais( + operacao, + sensoriamento, + trajetoria, + simulacaoAtual + ); + + if (!sensoriamento.Operacao.OperacaoIniciada && + btnIniciarSimulacao.Text.Contains("Parar")) { - simulacao = _Controle.SimulacaoMPC; + parar = true; + btnIniciarSimulacao.Text = "Iniciar Simulação"; + parar = false; } - else + } + + private void AtualizarCamposPosicaoSimuladaNaTela() + { + GPSModel posicaoExibicao; + double velocidadeExibicao; + double anguloControleExibicao; + + lock (_estadoSimulacaoLock) { - simulacao = new List(); + posicaoExibicao = posicaoAtual?.Clone(); + velocidadeExibicao = velocidadeCarroMs; + anguloControleExibicao = _ultimoAnguloControleAplicado; } - if (_Trajetoria?.CorredorAtual != null || (_Trajetoria?.RetornandoBase ?? false)) + if (posicaoExibicao != null) + { + txtAnguloGPS.Text = + posicaoExibicao.OrientacaoReal.ToString("0.00"); + txtVelocidade.Text = + velocidadeExibicao.ToString("0.00"); + txtDistanciaGPS.Text = + posicaoExibicao.Distancia.ToString("0.0000"); + + double latitudeExibicao = + CoordenadaGeograficaValida( + posicaoExibicao.LatitudeAnt, + posicaoExibicao.LongitudeAnt) + ? posicaoExibicao.LatitudeAnt + : posicaoExibicao.Latitude; + + double longitudeExibicao = + CoordenadaGeograficaValida( + posicaoExibicao.LatitudeAnt, + posicaoExibicao.LongitudeAnt) + ? posicaoExibicao.LongitudeAnt + : posicaoExibicao.Longitude; + + txtLatitude.Text = latitudeExibicao.ToString(); + txtLongitude.Text = longitudeExibicao.ToString(); + txtAnguloControle.Text = + anguloControleExibicao.ToString("0.00"); + } + } + + private void AtualizarMapasVisuais( + OperacaoModel operacao, + OperacaoSensoriamentoConjuntoModel sensoriamento, + TrajetoriaMapaOperacaoModel trajetoria, + List simulacaoAtual) + { + if (MapaDinamico == null || sensoriamento?.Gps == null) + return; + + List trajetoriaVisual = + CriarTrajetoriaVisual( + trajetoria._TrajetoriaDinamica, + MaxPontosTrajetoriaVisual + ); + + List corredorVisual = + trajetoria.RetornandoBase + ? new List() + : CriarTrajetoriaVisual( + trajetoria.CorredorAtual?.Pontos, + MaxPontosCorredorVisual + ); + + List trilhaVisual = ObterTrilhaVisual(); + List> ruasVisual = + ObterRuasVisuais(trajetoria.RuasPlantacao); + + if (trajetoria.CorredorAtual != null || + trajetoria.RetornandoBase) { MapaDinamico.AtualizarDados( - KinectService.Iniciado && KinectService.Leitura.StatusDetecao ? KinectService.Leitura.ObstaculoCritico : null, - (float)_Sensoriamento.Trajetoria.AnguloCaminho, - (float)_Sensoriamento.Gps.AnguloCarroDefinido, - _Sensoriamento.Gps, - _Trajetoria._TrajetoriaDinamica, - _Trajetoria.RetornandoBase ? new List() : _Trajetoria.CorredorAtual.Pontos, - Variaveis.OperacaoEmAndamento.GPSTrajetoria, - _Trajetoria.RuasPlantacao, - simulacao + KinectService.Iniciado && + KinectService.Leitura.StatusDetecao + ? KinectService.Leitura.ObstaculoCritico + : null, + (float)sensoriamento.Trajetoria.AnguloCaminho, + (float)sensoriamento.Gps.AnguloCarroDefinido, + sensoriamento.Gps, + trajetoriaVisual, + corredorVisual, + trilhaVisual, + ruasVisual, + simulacaoAtual ); } + bool publicarTrajetoria = + Interlocked.Exchange(ref _forcarTrajetoriaWeb, 0) == 1 || + _cronometroTrajetoriaWeb.ElapsedMilliseconds >= + IntervaloTrajetoriaWebMs; - if (!Variaveis.OperacaoEmAndamento.Sensoriamento.Operacao.OperacaoIniciada) + List rotaWeb = null; + + if (publicarTrajetoria) { - if (btnIniciarSimulacao.Text.Contains("Parar")) - { - parar = true; - btnIniciarSimulacao_Click(new object(), new EventArgs()); - parar = false; - } + _cronometroTrajetoriaWeb.Restart(); + rotaWeb = ConverterTrajetoriaParaGps(trajetoriaVisual); + } - //Variaveis.OperacaoEmAndamento.Mapa.AtualizarRuasSelecionadas(); + AtualizarMapaWebSemBloquear( + operacao.Mapa, + sensoriamento.Gps, + sensoriamento.Operacao.OperacaoIniciada, + publicarTrajetoria, + rotaWeb + ); + } + + private async void AtualizarMapaWebSemBloquear( + MapasModel mapa, + GPSModel gps, + bool emFoco, + bool publicarTrajetoria, + List rotaWeb) + { + if (mapa == null || gps == null || + Volatile.Read(ref _encerrando) == 1) + { + return; + } + + if (Interlocked.Exchange(ref _mapaWebEmExecucao, 1) == 1) + { + if (publicarTrajetoria) + Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1); + + return; + } + + try + { + // Este método é iniciado pela thread da UI. O await preserva + // o contexto necessário pelo WebView2. + await mapa.AtualizarPosicaoMapaAsync( + MapasVariaveisModel.IDMarcadorRover, + gps.Latitude, + gps.Longitude, + gps.AnguloCarroDefinido, + emFoco + ); + + if (publicarTrajetoria && rotaWeb != null) + await mapa.DefinirTrajetoriaPlanejadaAsync(rotaWeb); + } + catch (ObjectDisposedException) + { + } + catch (InvalidOperationException ex) + { + Variaveis.MostrarLog( + "[Simulacao] Atualização do mapa Web ignorada: " + + ex.Message + ); + } + catch (Exception ex) + { + Variaveis.MostrarLog( + "[Simulacao] Erro atualizando mapa Web: " + ex + ); + } + finally + { + Interlocked.Exchange(ref _mapaWebEmExecucao, 0); } } + #endregion - + #region Snapshots visuais limitados + + private static List CriarTrajetoriaVisual( + IList origem, + int limite) + { + if (origem == null || origem.Count == 0) + return new List(); + + limite = Math.Max(16, limite); + + for (int tentativa = 0; tentativa < 3; tentativa++) + { + try + { + int count = origem.Count; + int passo = Math.Max( + 1, + (int)Math.Ceiling(count / (double)limite) + ); + + var resultado = + new List( + Math.Min(count, limite + 64) + ); + + for (int i = 0; i < count; i++) + { + PontoTrajetoriaModel ponto = origem[i]; + if (ponto == null || ponto.Posicao == null) + continue; + + bool estrutural = + ponto.PontoBorda || + ponto.PontoLigacao || + ponto.Tipo == TipoPontoRua.BordaEntrada || + ponto.Tipo == TipoPontoRua.BordaSaida || + ponto.Tipo == TipoPontoRua.LigacaoEntrada || + ponto.Tipo == TipoPontoRua.LigacaoSaida; + + if (i == 0 || + i == count - 1 || + estrutural || + i % passo == 0) + { + if (resultado.Count == 0 || + !ReferenceEquals(resultado.Last(), ponto)) + { + resultado.Add(ponto); + } + } + } + + return resultado; + } + catch (ArgumentOutOfRangeException) + { + Thread.Yield(); + } + catch (InvalidOperationException) + { + Thread.Yield(); + } + } + + return new List(); + } + + private List ObterTrilhaVisual() + { + lock (_estadoSimulacaoLock) + return new List(_trilhaVisual); + } + + private List> ObterRuasVisuais( + IList> ruas) + { + if (ruas == null || ruas.Count == 0) + return new List>(); + + // As ruas da trajetória são imutáveis durante uma operação. + // Cachear por instância + quantidade evita percorrer milhares de + // pontos em todo frame apenas para validar o cache visual. + if (ReferenceEquals(_ruasFonteCache, ruas) && + ruas.Count == _ruasFonteQuantidadeRuas) + { + return _ruasVisualCache; + } + + var resultado = new List>(); + + for (int i = 0; i < ruas.Count; i++) + { + resultado.Add( + DecimarPontosGps( + ruas[i], + MaxPontosPorRuaVisual + ) + ); + } + + _ruasFonteCache = ruas; + _ruasFonteQuantidadeRuas = ruas.Count; + _ruasVisualCache = resultado; + + return resultado; + } + + private static List DecimarPontosGps( + IList origem, + int limite) + { + if (origem == null || origem.Count == 0) + return new List(); + + int count = origem.Count; + int passo = Math.Max( + 1, + (int)Math.Ceiling(count / (double)Math.Max(2, limite)) + ); + + var resultado = new List( + Math.Min(count, limite + 1) + ); + + for (int i = 0; i < count; i += passo) + { + GPSModel ponto = origem[i]; + if (ponto != null) + resultado.Add(ponto); + } + + GPSModel ultimo = origem[count - 1]; + if (ultimo != null && + (resultado.Count == 0 || + !ReferenceEquals(resultado[resultado.Count - 1], ultimo))) + { + resultado.Add(ultimo); + } + + return resultado; + } + + private static List ConverterTrajetoriaParaGps( + IList pontos) + { + var resultado = new List(pontos?.Count ?? 0); + + if (pontos == null) + return resultado; + + for (int i = 0; i < pontos.Count; i++) + { + GPSModel posicao = pontos[i]?.Posicao; + if (posicao == null) + continue; + + resultado.Add(new GPSModel() + { + Latitude = posicao.Latitude, + Longitude = posicao.Longitude + }); + } + + return resultado; + } + + private static List CopiarListaSegura(IList origem) + { + if (origem == null || origem.Count == 0) + return new List(); + + for (int tentativa = 0; tentativa < 3; tentativa++) + { + try + { + int count = origem.Count; + var resultado = new List(count); + + for (int i = 0; i < count; i++) + resultado.Add(origem[i]); + + return resultado; + } + catch (ArgumentOutOfRangeException) + { + Thread.Yield(); + } + catch (InvalidOperationException) + { + Thread.Yield(); + } + } + + return new List(); + } + + private static double CalcularDistanciaSimulacao( + IList pontos) + { + if (pontos == null || pontos.Count < 2) + return 0.0; + + double total = 0.0; + var anterior = new GPSModel() + { + Latitude = pontos[0].latitude, + Longitude = pontos[0].longitude + }; + + for (int i = 1; i < pontos.Count; i++) + { + var atual = new GPSModel() + { + Latitude = pontos[i].latitude, + Longitude = pontos[i].longitude + }; + + double distancia = + GPSUtils.DistanciaEntrePontos(anterior, atual); + + if (ValorFinito(distancia) && distancia >= 0) + total += distancia; + + anterior = atual; + } + + return total; + } + + #endregion + + #region Controles e comandos + + private void Mapa_RuasSelecionadasAlteradas( + object sender, + EventArgs e) + { + _ruasFonteCache = null; + _ruasFonteQuantidadeRuas = -1; + _ruasVisualCache = new List>(); + Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1); + } private void btnCarregar_Click(object sender, EventArgs e) { - Variaveis.OperacaoEmAndamento.Mapa.btnCarregar_Click((Control) sender, e); + Variaveis.OperacaoEmAndamento.Mapa.btnCarregar_Click( + (Control)sender, + e + ); } - private void btnIniciarGPS_Click(object sender, EventArgs e) + private bool TentarCriarPosicaoInicialDaTela( + out GPSModel posicao) { - posicaoAtual = new GPSModel() + posicao = null; + + double latitude; + double longitude; + double angulo; + + if (!TentarLerDouble(txtLatitude.Text, out latitude) || + !TentarLerDouble(txtLongitude.Text, out longitude) || + !TentarLerDouble(txtAnguloGPS.Text, out angulo) || + !CoordenadaGeograficaValida(latitude, longitude)) { - DataHora = DateTime.Now, - Lon0 = double.Parse(txtLongitude.Text.Replace(".", ",")), - LongitudeAnt = double.Parse(txtLongitude.Text.Replace(".", ",")), - Longitude = double.Parse(txtLongitude.Text.Replace(".", ",")), - LatitudeAnt = double.Parse(txtLatitude.Text.Replace(".", ",")), - Lat0 = double.Parse(txtLatitude.Text.Replace(".", ",")), - Latitude = double.Parse(txtLatitude.Text.Replace(".", ",")), - Heartbeat = GPSService.UltimaLeitura.Heartbeat, - OrientacaoReal = double.Parse(txtAnguloGPS.Text.Replace(".", ",")), - }; - AtualizarPosicaoGPS(); - btnAcrescentarGPS.Enabled = true; - txtAnguloGPS.Enabled = true; - txtDistanciaGPS.Enabled = true; - //btnIniciarGPS.Enabled = false; - //txtLatitude.ReadOnly = true; - //txtLongitude.ReadOnly = true; + return false; + } + + GPSModel atual = GPSService.GetSnapshot() ?? new GPSModel(); + + atual.DataHora = DateTime.Now; + atual.Momento = DateTime.Now; + atual.Lon0 = longitude; + atual.LongitudeAnt = longitude; + atual.Longitude = longitude; + atual.LatitudeAnt = latitude; + atual.Lat0 = latitude; + atual.Latitude = latitude; + atual.OrientacaoReal = GPSUtils.NormalizarAngulo(angulo); + atual.AnguloCarroDefinido = atual.OrientacaoReal; + atual.OrientacaoMovimento = atual.OrientacaoReal; + atual.TipoOrientacao = "A"; + atual.Distancia = 0.0; + atual.Velocidade = 0.0; + + posicao = atual; + return true; } - private void AtualizarPosicaoGPS() + private async void btnIniciarGPS_Click(object sender, EventArgs e) { - if (!GPSService.Iniciado || (txtLongitude.Text != "" && txtLatitude.Text != "")) + GPSModel nova; + + if (!TentarCriarPosicaoInicialDaTela(out nova)) { - double orientacaoReal = GPSUtils.NormalizarAngulo(posicaoAtual.OrientacaoReal); - - GPSService.UltimaLeitura.Momento = posicaoAtual.Momento; - GPSService.UltimaLeitura.DataHora = posicaoAtual.DataHora; - GPSService.UltimaLeitura.UltimoComandoRespondido = posicaoAtual.UltimoComandoRespondido; - - GPSService.UltimaLeitura.Latitude = posicaoAtual.Latitude; - GPSService.UltimaLeitura.Longitude = posicaoAtual.Longitude; - GPSService.UltimaLeitura.LatitudeAnt = posicaoAtual.LatitudeAnt; - GPSService.UltimaLeitura.LongitudeAnt = posicaoAtual.LongitudeAnt; - - GPSService.UltimaLeitura.OrientacaoReal = orientacaoReal; - GPSService.UltimaLeitura.AnguloCarroDefinido = orientacaoReal; - GPSService.UltimaLeitura.TipoOrientacao = "A"; - - GPSService.UltimaLeitura.OrientacaoMovimento = - double.IsNaN(posicaoAtual.OrientacaoMovimento) || double.IsInfinity(posicaoAtual.OrientacaoMovimento) - ? orientacaoReal - : GPSUtils.NormalizarAngulo(posicaoAtual.OrientacaoMovimento); - - GPSService.UltimaLeitura.Distancia = posicaoAtual.Distancia; - GPSService.UltimaLeitura.Velocidade = posicaoAtual.Velocidade; - - GPSService.UltimaLeitura.TimestampOri = posicaoAtual.TimestampOri.Clone(); - GPSService.UltimaLeitura.TimestampPos = posicaoAtual.TimestampPos.Clone(); - - GPSService.UltimaLeitura.Heartbeat = posicaoAtual.Heartbeat; - GPSService.UltimaLeitura.Inicializado = true; + MessageBox.Show( + "Latitude, longitude ou orientação inválida.", + "Simulação", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + return; } - GPSService.AtualizarAtrasoPosicoes(Convert.ToInt32(txtDelayPosicao.Text)); - GPSService.AtualizarCoordenadasGPS(); - } + AtualizarConfiguracaoSimulacaoDaTela(); - private async void btnAcrescentarGPS_Click(object sender, EventArgs e) - { - GPSModel novaPosicao = null; - velocidadeCarroMs = FuncoesMatematicas.CalculaVelocidadeMsPercentual(Variaveis.OperacaoEmAndamento.SimulacaoRpmControle); - if (Variaveis.OperacaoEmAndamento.Controle.TiposControle.FirstOrDefault(x => x.Tipo == T_Code.Mov).DirecaoAtual == Direcao.Tras) - velocidadeCarroMs *= -1; - //Console.WriteLine($"RPM SP: {Variaveis.OperacaoEmAndamento.SimulacaoRpmControle} ({velocidadeCarroMs:F2} ms)"); + PrepararLeituraSimulada( + nova, + 0.0, + Volatile.Read(ref _tempoGpsConfiguradoMs) + ); - Variaveis.OperacaoEmAndamento.DispMvd.Dados.VelocidadeMedia = velocidadeCarroMs; + btnIniciarGPS.Enabled = false; - double tempoPassado = - !chbAutomatico.Checked || UltimaAtualizacaoGps == DateTime.MinValue ? // esta no modo manual - (tempoGps / 1000.0) : // 1 tick do tempo gps - (DateTime.Now - UltimaAtualizacaoGps).TotalSeconds; // tempo decorrido entre ticks do timer de leitura - - // Obtém os parâmetros - double anguloAtual = double.Parse(txtAnguloGPS.Text); - - var _Gps = GPSService.UltimaLeitura; - - double anguloControle = 0.0; - TipoMovimentoDirecional tipoMovimento = Variaveis.OperacaoEmAndamento.Controle.TipoMovimento; - double veloccidade = Variaveis.OperacaoEmAndamento.Sensoriamento.Operacao.StatusOperacaoAtual != StatusOperacao.EmAndamento ? 0 : velocidadeCarroMs; - - var tipoControle = Variaveis.OperacaoEmAndamento.Parametros.Controle.DirTipoMovimento; - - if (passosOperacao.Count > 0 && Variaveis.OperacaoEmAndamento.Simulando) + try { - novaPosicao = passosOperacao[Math.Min(passosOperacao.Count - 1, idxPassoOperacao)]; - idxPassoOperacao++; - } - else - { - anguloControle = double.Parse(txtAnguloControle.Text); - - bool atualizarAnguloControle = (ticksControle >= ticksControleMax); - if (atualizarAnguloControle) + await Task.Run(delegate { - ticksControle = 0; - } - else - { - ticksControle++; - } + AtualizarPosicaoGPS( + nova, + Volatile.Read(ref _delayPosicaoPassos) + ); + }); - if (atualizarAnguloControle) - { - btnCalcularAngulo_Click(new object(), new EventArgs()); - anguloControle = double.Parse(txtAnguloControle.Text); - } - } + lock (_estadoSimulacaoLock) + posicaoAtual = nova.Clone(); - // Calcula a nova posicao do robo - //GPSModel pos = NovaPosicaoSimulacao(velocidadeMs, tempoPassado); - if (novaPosicao == null) - { - novaPosicao = VariaveisEquipamento.SimularNovaPosicao(tipoMovimento, anguloControle, veloccidade, tempoPassado, anguloAtual, _Gps); - } + btnAcrescentarGPS.Enabled = true; + txtAnguloGPS.Enabled = true; + txtDistanciaGPS.Enabled = true; - posicaoAtual = novaPosicao; - - - //Console.WriteLine($"C#: {DadosPosicao(novaPosicao)}"); - //Console.WriteLine($"Py: {DadosPosicao(pos)}"); - - // Atualiza a interface gráfica - txtAnguloGPS.Text = posicaoAtual.OrientacaoReal.ToString("0.00"); - txtVelocidade.Text = veloccidade.ToString("0.00"); - txtDistanciaGPS.Text = posicaoAtual.Distancia.ToString("0.0000"); - txtLatitude.Text = posicaoAtual.LatitudeAnt.ToString(); - txtLongitude.Text = posicaoAtual.LongitudeAnt.ToString(); - - UltimaAtualizacaoGps = DateTime.Now; - - AtualizarPosicaoGPS(); - - Variaveis.OperacaoEmAndamento.Sensoriamento.AtualizarDados(); - - if (!chbAutomatico.Checked) - { + Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1); + AtualizarCamposPosicaoSimuladaNaTela(); AtualizarDadosTela(); } - } - - private GPSModel NovaPosicaoSimulacao(double velocidadeMs, double tempo) - { - if (Variaveis.OperacaoEmAndamento.Controle.SimulacaoMPC.Count() > 1) + catch (Exception ex) { - double distancia = velocidadeMs * tempo; - MPCSimulacaoModel posicao = Variaveis.OperacaoEmAndamento.Controle.SimulacaoMPC[1]; - GPSModel novaPosicao = new GPSModel() - { - Latitude = posicao.latitude, - Longitude = posicao.longitude, - OrientacaoReal = posicao.orientacao, - Distancia = distancia, - }; - return novaPosicao; + Variaveis.MostrarLog( + "[Simulacao] Erro iniciando GPS simulado: " + ex + ); + MessageBox.Show( + "Não foi possível iniciar o GPS simulado.\n\n" + + ex.Message, + "Simulação", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + finally + { + if (!IsDisposed) + btnIniciarGPS.Enabled = true; } - return null; } - private string DadosPosicao(GPSModel pos) + private async void btnAcrescentarGPS_Click( + object sender, + EventArgs e) { - if (pos == null) return ""; - return $"Lat: {pos.Latitude}, Long: {pos.Longitude}, Theta: {pos.OrientacaoReal}, Distancia: {pos.Distancia}"; + if (Interlocked.Exchange( + ref _passoManualEmExecucao, + 1) == 1) + { + return; + } + + AtualizarConfiguracaoSimulacaoDaTela(); + btnAcrescentarGPS.Enabled = false; + + try + { + await Task.Run(() => + ExecutarPassoSimulacaoAsync( + CancellationToken.None, + aguardarGate: true + ) + ); + + AtualizarCamposPosicaoSimuladaNaTela(); + AtualizarDadosTela(); + } + catch (Exception ex) + { + Variaveis.MostrarLog( + "[Simulacao] Erro no passo manual: " + ex + ); + } + finally + { + Interlocked.Exchange(ref _passoManualEmExecucao, 0); + + if (!IsDisposed) + btnAcrescentarGPS.Enabled = true; + } } - private void btnIniciarSimulacao_Click(object sender, EventArgs e) + private async void btnIniciarSimulacao_Click( + object sender, + EventArgs e) { - if (!Variaveis.OperacaoEmAndamento.Mapa.RuasPercorrer.Any()) + var operacao = Variaveis.OperacaoEmAndamento; + + if (operacao?.Mapa?.RuasPercorrer == null || + !operacao.Mapa.RuasPercorrer.Any()) { MessageBox.Show("Selecione as ruas para realizar a operação!"); return; } - - if (!Variaveis.OperacaoEmAndamento.Sensoriamento.Operacao.OperacaoIniciada && !parar) + + try { - Variaveis.OperacaoEmAndamento.IniciarSimulacao(); + if (!operacao.Sensoriamento.Operacao.OperacaoIniciada && + !parar) + { + // O operador pode alterar o ângulo depois de posicionar o + // carro. Sincroniza a edição final imediatamente antes de + // iniciar para que o primeiro passo use esse heading. + GPSModel posicaoInicial; + + if (!TentarCriarPosicaoInicialDaTela( + out posicaoInicial)) + { + MessageBox.Show( + "Latitude, longitude ou orientação inválida.", + "Simulação", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + return; + } + + PrepararLeituraSimulada( + posicaoInicial, + 0.0, + Volatile.Read(ref _tempoGpsConfiguradoMs) + ); + + await Task.Run(delegate + { + AtualizarPosicaoGPS( + posicaoInicial, + Volatile.Read(ref _delayPosicaoPassos) + ); + }); + + lock (_estadoSimulacaoLock) + posicaoAtual = posicaoInicial.Clone(); + + await operacao.IniciarSimulacao(); + } + + bool iniciada = + operacao.Sensoriamento.Operacao.OperacaoIniciada; + + btnIniciarSimulacao.Text = iniciada + ? "Parar Simulação" + : "Iniciar Simulação"; + + if (iniciada) + { + GPSService.PenultimaLeitura.Latitude = + GPSService.UltimaLeitura.Latitude; + GPSService.PenultimaLeitura.Longitude = + GPSService.UltimaLeitura.Longitude; + + lock (_estadoSimulacaoLock) + _trilhaVisual.Clear(); + + Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1); + AtualizarDadosTela(); + } } - - btnIniciarSimulacao.Text = Variaveis.OperacaoEmAndamento.Sensoriamento.Operacao.OperacaoIniciada ? "Parar Simulação" : "Iniciar Simulação"; - if (Variaveis.OperacaoEmAndamento.Sensoriamento.Operacao.OperacaoIniciada) + catch (Exception ex) { - GPSService.PenultimaLeitura.Latitude = GPSService.UltimaLeitura.Latitude; - GPSService.PenultimaLeitura.Longitude = GPSService.UltimaLeitura.Longitude; - //Variaveis.OperacaoEmAndamento.Mapa.GerarTrajetoriaDinamica(); - - AtualizarDadosTela(); + Variaveis.MostrarLog( + "[Simulacao] Erro iniciando simulação: " + ex + ); + MessageBox.Show( + "Não foi possível iniciar a simulação.\n\n" + + ex.Message, + "Simulação", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); } } private void btnCarregarMapa_Click(object sender, EventArgs e) { - Variaveis.OperacaoEmAndamento.Mapa.btnCarregar_Click((Control)sender, e); + Variaveis.OperacaoEmAndamento.Mapa.btnCarregar_Click( + (Control)sender, + e + ); } private void btnCarregarOperacao_Click(object sender, EventArgs e) { - passosOperacao = new List(); - txtTempoGPS.Text = tempoGps.ToString(); - idxPassoOperacao = 0; - lblCarregado.Text = "Simulador"; - - OpenFileDialog ofd = new OpenFileDialog(); - ofd.Filter = "Arquivos de Operação|*.lgop"; - ofd.InitialDirectory = Application.StartupPath + "\\" + Variaveis.CaminhoOperacoes; - if (ofd.ShowDialog() == DialogResult.OK) + using (var ofd = new OpenFileDialog()) { - OperacaoDadosModel data = JsonConvert.DeserializeObject(File.ReadAllText(ofd.FileName)); + ofd.Filter = "Arquivos de Operação|*.lgop"; + ofd.InitialDirectory = + Application.StartupPath + "\\" + + Variaveis.CaminhoOperacoes; - passosOperacao = JsonConvert.DeserializeObject(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, T_Code.Gps.ToString())).ToList(); + if (ofd.ShowDialog() != DialogResult.OK) + return; - txtTempoGPS.Text = "1000"; - lblCarregado.Text = "Operação"; + try + { + // Mantém a validação do contêiner da operação. + JsonConvert.DeserializeObject( + File.ReadAllText(ofd.FileName) + ); + + GPSModel[] dados = + JsonConvert.DeserializeObject( + OperacaoModel.DeserializarDadosOperacao( + ofd.FileName, + T_Code.Gps.ToString() + ) + ) ?? Array.Empty(); + + DefinirPassosOperacao(dados.ToList()); + + txtTempoGPS.Text = "1000"; + lblCarregado.Text = "Operação"; + AtualizarConfiguracaoSimulacaoDaTela(); + } + catch (Exception ex) + { + MessageBox.Show( + "Não foi possível carregar a operação.\n\n" + + ex.Message, + "Simulação", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } } } private void btnCarregarTrajetoria_Click(object sender, EventArgs e) { - passosOperacao = new List(); - txtTempoGPS.Text = tempoGps.ToString(); - idxPassoOperacao = 0; - lblCarregado.Text = "Simulador"; - - OpenFileDialog ofd = new OpenFileDialog(); - ofd.Filter = "Arquivos de Trajetória|*.json"; - ofd.InitialDirectory = Application.StartupPath + "\\" + Variaveis.CaminhoMapasConvertidos; - if (ofd.ShowDialog() == DialogResult.OK) + using (var ofd = new OpenFileDialog()) { - var DadosMapa = JsonConvert.DeserializeObject(File.ReadAllText(ofd.FileName)); + ofd.Filter = "Arquivos de Trajetória|*.json"; + ofd.InitialDirectory = + Application.StartupPath + "\\" + + Variaveis.CaminhoMapasConvertidos; - passosOperacao = DadosMapa.features[0].geometry.coordinates - .Select(x => new GPSModel() - { - Latitude = x[1], - Longitude = x[0] - }) - .ToList(); + if (ofd.ShowDialog() != DialogResult.OK) + return; - txtTempoGPS.Text = "200"; - lblCarregado.Text = "Trajetória"; + try + { + var dadosMapa = + JsonConvert.DeserializeObject( + File.ReadAllText(ofd.FileName) + ); + + var coordenadas = dadosMapa? + .features? + .FirstOrDefault()? + .geometry? + .coordinates; + + if (coordenadas == null || coordenadas.Count == 0) + throw new InvalidDataException( + "O arquivo não contém uma trajetória válida." + ); + + var passos = coordenadas + .Where(x => x != null && x.Count >= 2) + .Select(x => new GPSModel() + { + Latitude = x[1], + Longitude = x[0], + LatitudeAnt = x[1], + LongitudeAnt = x[0] + }) + .ToList(); + + DefinirPassosOperacao(passos); + + txtTempoGPS.Text = "200"; + lblCarregado.Text = "Trajetória"; + AtualizarConfiguracaoSimulacaoDaTela(); + } + catch (Exception ex) + { + MessageBox.Show( + "Não foi possível carregar a trajetória.\n\n" + + ex.Message, + "Simulação", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } } } - private void pnlOrientacaoTrajeto_Paint(object sender, PaintEventArgs e) + private void DefinirPassosOperacao(List passos) { - try + lock (_estadoSimulacaoLock) { - //double angulo = Variaveis.OperacaoEmAndamento.Trajetoria.AnguloCaminho; - //double anguloCorredor = Variaveis.OperacaoEmAndamento.Trajetoria.AnguloMedioCorredor; - double angulo = Variaveis.OperacaoEmAndamento.Trajetoria?.ProximoPonto?.Orientacao ?? 0; - double anguloCorredor = Variaveis.OperacaoEmAndamento.Trajetoria?.AnguloCaminho ?? 0; - FuncoesGlobais.DesenharInclinacao((Panel)sender, e, (float)angulo, 90, (float)anguloCorredor); + passosOperacao = passos ?? new List(); + idxPassoOperacao = 0; + _trilhaVisual.Clear(); + historicoAngulosControle.Clear(); } - catch { } - } - private void pnlOrientacaoCarro_Paint(object sender, PaintEventArgs e) - { - try - { - double angulo = Variaveis.OperacaoEmAndamento.Sensoriamento?.Gps?.AnguloCarroDefinido ?? 0; - FuncoesGlobais.DesenharInclinacao((Panel)sender, e, (float)angulo, 90); - } - catch { } - } - - private void pnlAnguloControle_Paint(object sender, PaintEventArgs e) - { - try - { - double angulo = Variaveis.OperacaoEmAndamento.SimulacaoAnguloControle; - double dif = Variaveis.OperacaoEmAndamento.Sensoriamento?.Trajetoria?.ErroOrientacaoAngularProximoPonto ?? 0; - FuncoesGlobais.DesenharInclinacao((Panel)sender, e, (float)angulo, 90, (float)dif); - } - catch { } + Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1); } private void btnAtualizarLeitura_Click(object sender, EventArgs e) { + AtualizarConfiguracaoSimulacaoDaTela(); AtualizarDadosTela(); } private void btnCalcularAngulo_Click(object sender, EventArgs e) { - historicoAngulosControle.Enqueue(Variaveis.OperacaoEmAndamento.SimulacaoAnguloControle); - int errosConsiderar = int.Parse(txtDelayComando.Text); - while (historicoAngulosControle.Count > 1 && (historicoAngulosControle.Count > errosConsiderar || errosConsiderar == 0)) + AtualizarConfiguracaoSimulacaoDaTela(); + + double angulo = CalcularAnguloControleAtrasado( + Volatile.Read(ref _delayComandoPassos) + ); + + lock (_estadoSimulacaoLock) + _ultimoAnguloControleAplicado = angulo; + + txtAnguloControle.Text = angulo.ToString("0.00"); + } + + private double CalcularAnguloControleAtrasado(int errosConsiderar) + { + double novoAngulo = + Variaveis.OperacaoEmAndamento?.SimulacaoAnguloControle ?? 0.0; + + if (!ValorFinito(novoAngulo)) + novoAngulo = 0.0; + + lock (_estadoSimulacaoLock) { - historicoAngulosControle.Dequeue(); + historicoAngulosControle.Enqueue(novoAngulo); + + while (historicoAngulosControle.Count > 1 && + (historicoAngulosControle.Count > errosConsiderar || + errosConsiderar == 0)) + { + historicoAngulosControle.Dequeue(); + } + + _ultimoAnguloControleAplicado = + historicoAngulosControle.Peek(); + + return _ultimoAnguloControleAplicado; } - - txtAnguloControle.Text = historicoAngulosControle.Peek().ToString("0.00"); } - private void chbSonarVirando_CheckedChanged(object sender, EventArgs e) + private double ObterUltimoAnguloControle() { - + lock (_estadoSimulacaoLock) + return _ultimoAnguloControleAplicado; } - private void chbAutomatico_CheckedChanged(object sender, EventArgs e) + private void chbSonarVirando_CheckedChanged( + object sender, + EventArgs e) { - UltimaAtualizacaoGps = DateTime.MinValue; + } + + private void chbAutomatico_CheckedChanged( + object sender, + EventArgs e) + { + Interlocked.Exchange( + ref _automatico, + chbAutomatico.Checked ? 1 : 0 + ); } private void btnReiniciar_Click(object sender, EventArgs e) { + EncerrarRecursos(); Variaveis.OperacaoEmAndamento.ReiniciarSimulacao(); - frmSimulacaoMapaGPS frm = new frmSimulacaoMapaGPS(); + + var frm = new frmSimulacaoMapaGPS(); frm.Show(); - - this.Close(); + Close(); } - private void cmbTipoControleDirecional_SelectedIndexChanged(object sender, EventArgs e) + private void cmbTipoControleDirecional_SelectedIndexChanged( + object sender, + EventArgs e) { - Variaveis.OperacaoEmAndamento.Parametros.Controle.DirTipoMovimento = (TiposControladorDirecional)cmbTipoControleDirecional.SelectedIndex; - RedisService.AtualizarCampos(CtxKey.DadosOperacao, ("configurado", false)); + if (cmbTipoControleDirecional.SelectedIndex < 0 || + Variaveis.OperacaoEmAndamento?.Parametros?.Controle == null) + { + return; + } + + Variaveis.OperacaoEmAndamento.Parametros.Controle + .DirTipoMovimento = + (TiposControladorDirecional) + cmbTipoControleDirecional.SelectedIndex; + + RedisService.AtualizarCampos( + CtxKey.DadosOperacao, + ("configurado", false) + ); } - private void cmbStatusCorredor_SelectedIndexChanged(object sender, EventArgs e) + private void cmbStatusCorredor_SelectedIndexChanged( + object sender, + EventArgs e) { var json = RedisService.Get(CtxKey.DadosVisualWorker); - if (string.IsNullOrWhiteSpace(json)) return; + if (string.IsNullOrWhiteSpace(json)) + return; + JObject root; try { @@ -601,52 +1706,285 @@ namespace AgroBase.Forms { return; } + long tsUnix = root["ts_analise"]?.Value() ?? 0; + RedisService.AtualizarCampos( CtxKey.DadosVisualWorker, ("ts_analise", tsUnix + 1), - ("segmentacao.status_corredor", cmbStatusCorredor.SelectedIndex) + ( + "segmentacao.status_corredor", + cmbStatusCorredor.SelectedIndex + ) ); } - private void chbAcompanharCarro_CheckedChanged(object sender, EventArgs e) + private void chbAcompanharCarro_CheckedChanged( + object sender, + EventArgs e) { - MapaDinamico.AlterarTravaRover(chbAcompanharCarro.Checked); + MapaDinamico?.AlterarTravaRover( + chbAcompanharCarro.Checked + ); } - - - private void btnRetornarBase_Click(object sender, EventArgs e) { - //OperacaoModel.CarregarParametrosOperacaoPadrao(ModoOperacao.RetornoBase, true); - Variaveis.OperacaoEmAndamento.Trajetoria.IniciarRetornoBase(VariaveisOperacao.PosicaoBase); - Variaveis.OperacaoEmAndamento.Simulando = true; + var operacao = Variaveis.OperacaoEmAndamento; + if (operacao?.Trajetoria == null) + return; + + operacao.Trajetoria.IniciarRetornoBase( + VariaveisOperacao.PosicaoBase + ); + operacao.Simulando = true; + + Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1); } private void btnFixarBase_Click(object sender, EventArgs e) { + double latitude; + double longitude; + double angulo; + + if (!TentarLerDouble(txtLatitude.Text, out latitude) || + !TentarLerDouble(txtLongitude.Text, out longitude) || + !TentarLerDouble(txtAnguloGPS.Text, out angulo)) + { + MessageBox.Show( + "Latitude, longitude ou orientação inválida.", + "Simulação", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + return; + } + VariaveisOperacao.PosicaoBase = new GPSModel() { DataHora = DateTime.Now, - Lon0 = double.Parse(txtLongitude.Text.Replace(".", ",")), - LongitudeAnt = double.Parse(txtLongitude.Text.Replace(".", ",")), - Longitude = double.Parse(txtLongitude.Text.Replace(".", ",")), - LatitudeAnt = double.Parse(txtLatitude.Text.Replace(".", ",")), - Lat0 = double.Parse(txtLatitude.Text.Replace(".", ",")), - Latitude = double.Parse(txtLatitude.Text.Replace(".", ",")), + Momento = DateTime.Now, + Lon0 = longitude, + LongitudeAnt = longitude, + Longitude = longitude, + LatitudeAnt = latitude, + Lat0 = latitude, + Latitude = latitude, Heartbeat = GPSService.UltimaLeitura.Heartbeat, - OrientacaoReal = double.Parse(txtAnguloGPS.Text.Replace(".", ",")), + OrientacaoReal = angulo, + AnguloCarroDefinido = angulo }; - GPSService.EnviarCoordenadasParaMapa(VariaveisOperacao.PosicaoBase.Latitude, VariaveisOperacao.PosicaoBase.Longitude, VariaveisOperacao.PosicaoBase.AnguloCarroDefinido, false, rover: false); + + GPSService.EnviarCoordenadasParaMapa( + VariaveisOperacao.PosicaoBase.Latitude, + VariaveisOperacao.PosicaoBase.Longitude, + VariaveisOperacao.PosicaoBase.AnguloCarroDefinido, + false, + rover: false + ); } private void btnLiberarCorredor_Click(object sender, EventArgs e) { - int idx_corredor = (Variaveis.OperacaoEmAndamento.Trajetoria.CorredorAtual?.Idx ?? 0) + (Variaveis.OperacaoEmAndamento.Trajetoria.CorredorAtual?.Concluido ?? false ? 1 : 0); - Variaveis.OperacaoEmAndamento.Trajetoria.AtualizarDadosAutonomiaCorredor(idxCorredor: idx_corredor, bat_liberada: true, herb_liberado: true); - } - } + var trajetoria = Variaveis.OperacaoEmAndamento?.Trajetoria; + if (trajetoria == null) + return; - + int idxCorredor = + (trajetoria.CorredorAtual?.Idx ?? 0) + + ((trajetoria.CorredorAtual?.Concluido ?? false) ? 1 : 0); + + trajetoria.AtualizarDadosAutonomiaCorredor( + idxCorredor: idxCorredor, + bat_liberada: true, + herb_liberado: true + ); + } + + #endregion + + #region Paints auxiliares + + private void pnlOrientacaoTrajeto_Paint( + object sender, + PaintEventArgs e) + { + try + { + double angulo = + Variaveis.OperacaoEmAndamento + .Trajetoria? + .ProximoPonto? + .Orientacao ?? 0; + + double anguloCorredor = + Variaveis.OperacaoEmAndamento + .Trajetoria? + .AnguloCaminho ?? 0; + + FuncoesGlobais.DesenharInclinacao( + (Panel)sender, + e, + (float)angulo, + 90, + (float)anguloCorredor + ); + } + catch + { + } + } + + private void pnlOrientacaoCarro_Paint( + object sender, + PaintEventArgs e) + { + try + { + double angulo = + Variaveis.OperacaoEmAndamento + .Sensoriamento? + .Gps? + .AnguloCarroDefinido ?? 0; + + FuncoesGlobais.DesenharInclinacao( + (Panel)sender, + e, + (float)angulo, + 90 + ); + } + catch + { + } + } + + private void pnlAnguloControle_Paint( + object sender, + PaintEventArgs e) + { + try + { + double angulo = + Variaveis.OperacaoEmAndamento + .SimulacaoAnguloControle; + + double diferenca = + Variaveis.OperacaoEmAndamento + .Sensoriamento? + .Trajetoria? + .ErroOrientacaoAngularProximoPonto ?? 0; + + FuncoesGlobais.DesenharInclinacao( + (Panel)sender, + e, + (float)angulo, + 90, + (float)diferenca + ); + } + catch + { + } + } + + #endregion + + #region Utilidades + + private static bool TentarLerDouble( + string texto, + out double valor) + { + if (double.TryParse( + texto, + NumberStyles.Float, + CultureInfo.CurrentCulture, + out valor)) + { + return ValorFinito(valor); + } + + string normalizado = + (texto ?? string.Empty).Trim().Replace(',', '.'); + + if (double.TryParse( + normalizado, + NumberStyles.Float, + CultureInfo.InvariantCulture, + out valor)) + { + return ValorFinito(valor); + } + + valor = 0.0; + return false; + } + + private static bool CoordenadaGeograficaValida( + double latitude, + double longitude) + { + return ValorFinito(latitude) && + ValorFinito(longitude) && + latitude >= -90.0 && latitude <= 90.0 && + longitude >= -180.0 && longitude <= 180.0 && + !(Math.Abs(latitude) < 0.0000001 && + Math.Abs(longitude) < 0.0000001); + } + + private static bool ValorFinito(double valor) + { + return !double.IsNaN(valor) && !double.IsInfinity(valor); + } + + private static int LimitarInteiro( + int valor, + int minimo, + int maximo) + { + return Math.Max(minimo, Math.Min(maximo, valor)); + } + + // Mantidos porque podem ser usados durante depuração manual do form. + private GPSModel NovaPosicaoSimulacao( + double velocidadeMs, + double tempo) + { + List pontos = + CopiarListaSegura( + Variaveis.OperacaoEmAndamento? + .Controle? + .SimulacaoMPC + ); + + if (pontos.Count <= 1) + return null; + + MPCSimulacaoModel posicao = pontos[1]; + + return new GPSModel() + { + Latitude = posicao.latitude, + Longitude = posicao.longitude, + LatitudeAnt = posicao.latitude, + LongitudeAnt = posicao.longitude, + OrientacaoReal = posicao.orientacao, + Distancia = velocidadeMs * tempo, + }; + } + + private string DadosPosicao(GPSModel pos) + { + if (pos == null) + return string.Empty; + + return + $"Lat: {pos.Latitude}, Long: {pos.Longitude}, " + + $"Theta: {pos.OrientacaoReal}, Distancia: {pos.Distancia}"; + } + + #endregion + } } diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs index 8686b9426..37425f1d3 100644 --- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs +++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs @@ -1959,7 +1959,7 @@ namespace AgroBase.Models var dispMvd = DispMvd; if (dispMvd?.Dados?.Modulos != null) // && value != Controle.TipoMovimento - { + { Controle.DefinirTipoMovimento(value, dispMvd.Dados.Modulos); } } diff --git a/AgroBase/AgroBase/Services/GPSService.cs b/AgroBase/AgroBase/Services/GPSService.cs index cb50a9e72..01984a431 100644 --- a/AgroBase/AgroBase/Services/GPSService.cs +++ b/AgroBase/AgroBase/Services/GPSService.cs @@ -12,6 +12,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Windows.Forms; using static AgroBase.Models.Enums; namespace AgroBase.Services @@ -171,6 +172,94 @@ namespace AgroBase.Services private static string _lastError; private static readonly object _metricsTextLock = new object(); + + + private const int IntervaloTrajetoriaMapaMs = 1_000; + private const int MaxPontosTrajetoriaMapa = 800; + + private static int _publicacaoMapaAutomaticaHabilitada = 1; + private static long _ultimaTrajetoriaMapaMono; + + /// + /// Permite que uma tela seja dona da publicação visual sem interromper o + /// processamento GNSS, Redis, trajetória ou histórico da operação. + /// O valor padrão preserva o comportamento legado. + /// + public static bool PublicacaoMapaAutomaticaHabilitada + { + get + { + return Volatile.Read( + ref _publicacaoMapaAutomaticaHabilitada + ) == 1; + } + set + { + Volatile.Write( + ref _publicacaoMapaAutomaticaHabilitada, + value ? 1 : 0 + ); + } + } + + public static void AtualizarLeituraSimulada(GPSModel leitura, int atrasoPosicaoPassos = 0) + { + if (leitura == null) + throw new ArgumentNullException(nameof(leitura)); + + lock (_stateLock) + { + double orientacaoReal = + GPSUtils.NormalizarAngulo(leitura.OrientacaoReal); + + UltimaLeitura.Momento = leitura.Momento; + UltimaLeitura.DataHora = leitura.DataHora; + UltimaLeitura.UltimoComandoRespondido = + leitura.UltimoComandoRespondido; + + UltimaLeitura.Latitude = leitura.Latitude; + UltimaLeitura.Longitude = leitura.Longitude; + UltimaLeitura.LatitudeAnt = leitura.LatitudeAnt; + UltimaLeitura.LongitudeAnt = leitura.LongitudeAnt; + + UltimaLeitura.OrientacaoReal = orientacaoReal; + UltimaLeitura.AnguloCarroDefinido = orientacaoReal; + UltimaLeitura.TipoOrientacao = "A"; + + UltimaLeitura.OrientacaoMovimento = + double.IsNaN(leitura.OrientacaoMovimento) || + double.IsInfinity(leitura.OrientacaoMovimento) + ? orientacaoReal + : GPSUtils.NormalizarAngulo( + leitura.OrientacaoMovimento + ); + + UltimaLeitura.Distancia = leitura.Distancia; + UltimaLeitura.Velocidade = leitura.Velocidade; + + if (leitura.TimestampOri != null) + UltimaLeitura.TimestampOri = + leitura.TimestampOri.Clone(); + + if (leitura.TimestampPos != null) + UltimaLeitura.TimestampPos = + leitura.TimestampPos.Clone(); + + UltimaLeitura.Heartbeat = leitura.Heartbeat; + UltimaLeitura.Inicializado = true; + + // Monitor/lock do C# é reentrante; os dois métodos abaixo usam o + // mesmo lock e permanecem em uma única transação de leitura GNSS. + AtualizarAtrasoPosicoes( + Math.Max(0, atrasoPosicaoPassos) + ); + AtualizarCoordenadasGPS(); + } + } + + + + // ============================================================ // INICIALIZAÇÃO DA PORTA // ============================================================ @@ -2198,14 +2287,30 @@ namespace AgroBase.Services op.Trajetoria.LoopAtualizaDados(); } - AtualizarTrajetoriaDinamica(); - - EnviarCoordenadasParaMapa(UltimaLeitura.Latitude, UltimaLeitura.Longitude, UltimaLeitura.AnguloCarroDefinido, op.Sensoriamento.Operacao.OperacaoIniciada, rover: true); - - GPSModel posicaoBase = VariaveisOperacao.ObterPosicaoBaseSnapshot(); - if ((posicaoBase?.UltimoComandoRespondido ?? DateTime.MinValue) > DateTime.UtcNow.AddSeconds(-60)) + if (PublicacaoMapaAutomaticaHabilitada) { - EnviarCoordenadasParaMapa(posicaoBase.Latitude, posicaoBase.Longitude, posicaoBase.OrientacaoReal, false, rover: false); + AtualizarTrajetoriaDinamica(); + + EnviarCoordenadasParaMapa( + UltimaLeitura.Latitude, + UltimaLeitura.Longitude, + UltimaLeitura.AnguloCarroDefinido, + op.Sensoriamento.Operacao.OperacaoIniciada, + rover: true + ); + + GPSModel posicaoBase = VariaveisOperacao.ObterPosicaoBaseSnapshot(); + + if ((posicaoBase?.UltimoComandoRespondido ?? DateTime.MinValue) > DateTime.UtcNow.AddSeconds(-60)) + { + EnviarCoordenadasParaMapa( + posicaoBase.Latitude, + posicaoBase.Longitude, + posicaoBase.OrientacaoReal, + false, + rover: false + ); + } } PenultimaLeitura.Inicializado = UltimaLeitura.Inicializado; @@ -2221,29 +2326,172 @@ namespace AgroBase.Services // PUBLICAÇÃO LOCAL LATEST-ONLY // ============================================================ - public static void EnviarCoordenadasParaMapa(double Latitude, double Longitude, double Orientacao, bool EmFoco, bool rover) + public static void EnviarCoordenadasParaMapa(double latitude, double longitude, double orientacao, bool emFoco, bool rover) { - int ID = rover ? MapasVariaveisModel.IDMarcadorRover : MapasVariaveisModel.IDMarcadorBase; - Variaveis.OperacaoEmAndamento?.Mapa?.AtualizarPosicaoMapaAsync(ID, Latitude, Longitude, Orientacao, EmFoco); + MapasModel mapa = Variaveis.OperacaoEmAndamento?.Mapa; + Panel painel = mapa?.pnlMapa; + + // Produto headless: retorna antes de tocar no WebView. + if (painel == null || painel.IsDisposed || !painel.IsHandleCreated) + { + return; + } + + int id = rover + ? MapasVariaveisModel.IDMarcadorRover + : MapasVariaveisModel.IDMarcadorBase; + + ExecutarMapaNaUi( + mapa, + delegate + { + Task tarefa = mapa.AtualizarPosicaoMapaAsync( + id, + latitude, + longitude, + orientacao, + emFoco + ); + + ObservarFalhaMapa( + tarefa, + "GPSService.EnviarCoordenadasParaMapa" + ); + } + ); } public static void AtualizarTrajetoriaDinamica() { var op = Variaveis.OperacaoEmAndamento; + var trajetoria = op?.Trajetoria; + MapasModel mapa = op?.Mapa; + Panel painel = mapa?.pnlMapa; - var dinamica = op.Trajetoria?._TrajetoriaDinamica; + // No produto sem UI, não cria cópia nem serializa a trajetória. + if (trajetoria == null || painel == null || painel.IsDisposed || !painel.IsHandleCreated) + { + return; + } - if (!(dinamica?.Any() ?? false)) + long anterior = Interlocked.Read(ref _ultimaTrajetoriaMapaMono); + + if (GetAgeMs(anterior) < IntervaloTrajetoriaMapaMs) return; - var trajetoria = op.Trajetoria.TrajetoriaDinamica.Select(x => new - { - latitude = x.Latitude, - longitude = x.Longitude - }) - .ToArray(); + long agora = Stopwatch.GetTimestamp(); - op?.Mapa?.DefinirTrajetoriaPlanejadaAsync(op.Trajetoria.TrajetoriaDinamica); + // Caso duas leituras cheguem juntas, somente uma publica. + if (Interlocked.CompareExchange(ref _ultimaTrajetoriaMapaMono, agora, anterior) != anterior) + { + return; + } + + List pontos = CriarTrajetoriaMapaVisual(trajetoria._TrajetoriaDinamica, MaxPontosTrajetoriaMapa); + + if (pontos.Count == 0) + return; + + ExecutarMapaNaUi( + mapa, + delegate + { + Task tarefa = mapa.DefinirTrajetoriaPlanejadaAsync(pontos); + + ObservarFalhaMapa( + tarefa, + "GPSService.AtualizarTrajetoriaDinamica" + ); + } + ); + } + + private static List CriarTrajetoriaMapaVisual(IList origem, int limite) + { + var resultado = new List(); + + if (origem == null || origem.Count == 0) + return resultado; + + int count = origem.Count; + int passo = Math.Max(1, (int)Math.Ceiling(count / (double)Math.Max(2, limite))); + + for (int i = 0; i < count; i++) + { + PontoTrajetoriaModel ponto = origem[i]; + + if (ponto?.Posicao == null) + continue; + + bool estrutural = + ponto.PontoBorda || + ponto.PontoLigacao || + ponto.Tipo == TipoPontoRua.BordaEntrada || + ponto.Tipo == TipoPontoRua.BordaSaida || + ponto.Tipo == TipoPontoRua.LigacaoEntrada || + ponto.Tipo == TipoPontoRua.LigacaoSaida; + + if (i != 0 && i != count - 1 && !estrutural && i % passo != 0) + { + continue; + } + + resultado.Add(new GPSModel() + { + Latitude = ponto.Posicao.Latitude, + Longitude = ponto.Posicao.Longitude + }); + } + + return resultado; + } + + private static void ExecutarMapaNaUi(MapasModel mapa, Action acao) + { + Panel painel = mapa?.pnlMapa; + + if (acao == null || painel == null || painel.IsDisposed || !painel.IsHandleCreated) + { + return; + } + + try + { + if (painel.InvokeRequired) + { + painel.BeginInvoke(acao); + return; + } + + acao(); + } + catch (ObjectDisposedException) + { + } + catch (InvalidOperationException) + { + // A janela pode estar fechando entre as validações. + } + } + + private static void ObservarFalhaMapa(Task tarefa, string origem) + { + if (tarefa == null) + return; + + _ = tarefa.ContinueWith( + t => + { + Exception erro = t.Exception?.GetBaseException(); + RecordError( + "[" + origem + "] " + + (erro?.Message ?? "Falha desconhecida no mapa.") + ); + }, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default + ); } private static async Task LocalPublisherLoopAsync(CancellationToken cancellationToken)