From 566e49c8441dd8c93cc33ddc3c76639e8f557f24 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Thu, 30 Jul 2026 15:09:41 -0300 Subject: [PATCH] ajustes para modo producao --- AgroBase/AgroBase/Forms/IHM/frmIHM.cs | 2 +- AgroBase/AgroBase/Forms/frmInstancial.cs | 2 +- .../Models/Operacoes/OperacaoModel.cs | 2 +- AgroBase/AgroBase/Models/PerformanceModel.cs | 2 + AgroBase/AgroBase/Models/Variaveis.cs | 43 +- .../Services/Operadores/OperadoresService.cs | 943 +++++++++++++----- 6 files changed, 763 insertions(+), 231 deletions(-) diff --git a/AgroBase/AgroBase/Forms/IHM/frmIHM.cs b/AgroBase/AgroBase/Forms/IHM/frmIHM.cs index c16271afd..7a6122326 100644 --- a/AgroBase/AgroBase/Forms/IHM/frmIHM.cs +++ b/AgroBase/AgroBase/Forms/IHM/frmIHM.cs @@ -496,7 +496,7 @@ namespace AgroBase.Forms.IHM // ========================================================= // TEMPERATURA // ========================================================= - double temperatura = operacao?.Sensoriamento?.CoolerControl?.Temperatura ?? 0; + double temperatura = operacao?.Sensoriamento?.DadosPerformance?.Temperatura ?? 0; var temperaturaSaude = ObterSaudeModulo(saudeModulos, Enums.T_Code.Npc); Color corTemperatura = FuncoesIHM.CorTemperatura(temperatura, temperaturaSaude.saude); AtualizarTemperatura($"{temperatura:0.#} °C", corTemperatura); diff --git a/AgroBase/AgroBase/Forms/frmInstancial.cs b/AgroBase/AgroBase/Forms/frmInstancial.cs index c3279b5b6..f4a9133eb 100644 --- a/AgroBase/AgroBase/Forms/frmInstancial.cs +++ b/AgroBase/AgroBase/Forms/frmInstancial.cs @@ -387,7 +387,7 @@ namespace AgroBase.Forms try { - Environment.Exit(1); + Environment.Exit(Variaveis.ExitShutdownCode); } catch { } } diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs index c9a37b8b5..b86167a0d 100644 --- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs +++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs @@ -5045,7 +5045,7 @@ namespace AgroBase.Models _Reservatorio?.AtualizarAgitadorCalda(op.Parametros?.Controle?.AtuAgitadorModo ?? ModoAgitadorCalda.SemAgitacao); var _Cooler = _DispSen?.Dados?.CoolerControl; - _Cooler?.Atualizar(Math.Max(DadosPerformance?.GPU_temp ?? 0, DadosPerformance?.CPU_temp ?? 0), agora); + _Cooler?.Atualizar(DadosPerformance?.Temperatura ?? 0, agora); // Clones diff --git a/AgroBase/AgroBase/Models/PerformanceModel.cs b/AgroBase/AgroBase/Models/PerformanceModel.cs index 7192250d3..48df434bd 100644 --- a/AgroBase/AgroBase/Models/PerformanceModel.cs +++ b/AgroBase/AgroBase/Models/PerformanceModel.cs @@ -123,6 +123,8 @@ namespace AgroBase.Models ? 0 : 100.0f * ThreadPool_used_completion_port_threads / ThreadPool_max_completion_port_threads; + public double Temperatura => Math.Max(GPU_temp_core_c ?? 0, CPU_temp_package_c ?? 0); + public string PerformanceStr => $"CPU={Fmt(CPU_load_percent)}% CPU_T={Fmt(CPU_temp_package_c)}C RAM={Fmt(RAM_usage_percent)}% " + $"GPU={Fmt(GPU_load_percent)}% GPU_Core={Fmt(GPU_core_load_percent)}% GPU_Mem={Fmt(GPU_memory_load_percent)}% " + $"VRAM={Fmt(GPU_vram_used_mb)}/{Fmt(GPU_vram_total_mb)}MB GPU_T={Fmt(GPU_temp_core_c)}C " + diff --git a/AgroBase/AgroBase/Models/Variaveis.cs b/AgroBase/AgroBase/Models/Variaveis.cs index 187fda894..a54a7ee5e 100644 --- a/AgroBase/AgroBase/Models/Variaveis.cs +++ b/AgroBase/AgroBase/Models/Variaveis.cs @@ -31,6 +31,7 @@ namespace AgroBase.Models public static readonly bool IniciarWorkers = true; public static readonly bool UsarIHM = true; public static readonly bool Producao = true; + public static readonly int ExitShutdownCode = 100; public static bool DebugMode { get; set; } = false; public static bool Fechando { get; set; } = false; @@ -2173,10 +2174,44 @@ namespace AgroBase.Models public static DateTime UnixToDateTime(string unixTime) { - double time = Convert.ToDouble(unixTime.Replace(".", ",")); - var dateTime = DateTimeOffset.FromUnixTimeMilliseconds((long)(time * 1000)).ToLocalTime(); - DateTime momento = dateTime.DateTime; - return momento; + if (string.IsNullOrWhiteSpace(unixTime)) + return DateTime.MinValue; + + if (!decimal.TryParse(unixTime, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out decimal timestamp)) + { + return DateTime.MinValue; + } + + try + { + // Unix em segundos, inclusive com fração: + // 1785431515.93917 + if (timestamp >= -62135596800m && timestamp <= 253402300799.999m) + { + long milliseconds = decimal.ToInt64(decimal.Truncate(timestamp * 1000m)); + + return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).LocalDateTime; + } + + // Unix em milissegundos: + // 1785431515939 + if (timestamp >= -62135596800000m && timestamp <= 253402300799999m) + { + long milliseconds = decimal.ToInt64(decimal.Truncate(timestamp)); + + return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).LocalDateTime; + } + + return DateTime.MinValue; + } + catch (ArgumentOutOfRangeException) + { + return DateTime.MinValue; + } + catch (OverflowException) + { + return DateTime.MinValue; + } } } diff --git a/AgroBase/AgroBase/Services/Operadores/OperadoresService.cs b/AgroBase/AgroBase/Services/Operadores/OperadoresService.cs index 89b4242aa..aa710d88e 100644 --- a/AgroBase/AgroBase/Services/Operadores/OperadoresService.cs +++ b/AgroBase/AgroBase/Services/Operadores/OperadoresService.cs @@ -13,10 +13,27 @@ namespace AgroBase.Services.Operadores { public class OperadoresService { - private readonly object _estadoLock = new object(); - private readonly SemaphoreSlim _restartGate = new SemaphoreSlim(1, 1); + #region Constantes e configurações - private Process pythonProcess; + private const int IntervaloHeartbeatMs = 1000; + private const int TimeoutDesconexaoSegundos = 3; + public readonly int TimeoutIniciarWorkerMs = 10000; + + private static readonly TimeSpan GracePeriodInicializacao = TimeSpan.FromSeconds(30); + private static readonly TimeSpan IntervaloMinimoRestart = TimeSpan.FromSeconds(10); + private static readonly TimeSpan IntervaloLogDesconectado = TimeSpan.FromSeconds(5); + + #endregion + + #region Sincronização + + private readonly object _estadoLock = new object(); + private readonly SemaphoreSlim _cicloVidaGate = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _heartbeatGate = new SemaphoreSlim(1, 1); + + #endregion + + #region Workers e processo Python public List Workers = new List() { @@ -27,49 +44,62 @@ namespace AgroBase.Services.Operadores new WeedWorkerService(), }; - private bool iniciarScript = true; - private bool workersInicializados = false; - private bool encerrando = false; - - private int TimeoutDesconexao = 3; - private int TempoDesconectado = 0; - - private DateTime ultimoRestart = DateTime.MinValue; - private DateTime ultimoLogDesconectado = DateTime.MinValue; - - private readonly TimeSpan IntervaloMinimoRestart = TimeSpan.FromSeconds(10); - private readonly TimeSpan IntervaloLogDesconectado = TimeSpan.FromSeconds(5); - public readonly int TimeoutIniciarWorkerMs = 10000; - + private Process pythonProcess; private AsyncTaskTimerModel tmrHearthbeat; - public bool Conectado = false; - public bool TodosWorkersConectados = false; - public bool PythonRodando = false; - private bool encerrandoPythonIntencionalmente = false; + #endregion + + #region Estado do ciclo de vida + + private bool iniciarScript = true; + private bool workersInicializados; + private bool encerrando; + private bool encerrandoPythonIntencionalmente; + private bool recebeuPrimeiroHeartbeatValido; + + private DateTime inicioProcessoPythonUtc = DateTime.MinValue; + private DateTime ultimoRestartUtc = DateTime.MinValue; + private DateTime ultimoLogDesconectadoUtc = DateTime.MinValue; + private DateTime? desconectadoDesdeUtc; + + public bool Conectado; + public bool TodosWorkersConectados; + public bool PythonRodando; + public int TempoDesconectado; + + #endregion + + #region Inicialização public async Task IniciarProcessamento(bool _iniciarScript = true) { - iniciarScript = _iniciarScript; - encerrando = false; - - Variaveis.MostrarLog("[OperadoresService.IniciarProcessamento] Iniciando núcleo central de processamento..."); - - bool iniciou = await IniciarOperadoresAsync("inicio", inicializacaoCompleta: true); - - tmrHearthbeat?.Dispose(); - tmrHearthbeat = new AsyncTaskTimerModel("tmrHearthbeatOperadores", tmrHearthbeat_Tick, 1000); - tmrHearthbeat.Start(); - - if (iniciou) + lock (_estadoLock) { - Variaveis.MostrarLog("[OperadoresService.IniciarProcessamento] Núcleo central solicitado com sucesso."); - } - else - { - Variaveis.MostrarLog("[OperadoresService.IniciarProcessamento] Núcleo central iniciou com pendências. Heartbeat tentará recuperar."); + iniciarScript = _iniciarScript; + encerrando = false; } + Variaveis.MostrarLog( + "[OperadoresService.IniciarProcessamento] " + + "Iniciando núcleo central de processamento..." + ); + + bool iniciou = await IniciarOperadoresAsync( + "início", + inicializacaoCompleta: true + ); + + ReiniciarTimerHeartbeat(); + + Variaveis.MostrarLog( + iniciou + ? "[OperadoresService.IniciarProcessamento] " + + "Núcleo central solicitado com sucesso." + : "[OperadoresService.IniciarProcessamento] " + + "Núcleo central iniciou com pendências. " + + "O heartbeat tentará recuperar após o período de inicialização." + ); + return iniciou; } @@ -79,75 +109,140 @@ namespace AgroBase.Services.Operadores try { - entrou = await _restartGate.WaitAsync(0); + entrou = await _cicloVidaGate.WaitAsync(0); if (!entrou) { - Variaveis.MostrarLog("[OperadoresService.IniciarOperadoresAsync] Restart ignorado, já existe uma inicialização em andamento."); + Variaveis.MostrarLog( + "[OperadoresService.IniciarOperadoresAsync] " + + "Solicitação ignorada: já existe uma alteração de ciclo de vida em andamento." + ); + return false; } - ultimoRestart = DateTime.Now; - - Variaveis.MostrarLog($"[OperadoresService.IniciarOperadoresAsync] Iniciando operadores. Origem={origem}, iniciarScript={iniciarScript}"); - - List> tasksWorkers = new List>(); - - if (!workersInicializados || inicializacaoCompleta) + if (EstaEncerrando()) { - foreach (var worker in Workers) - { - tasksWorkers.Add(IniciarWorkerSeguroAsync(worker)); - } + Variaveis.MostrarLog( + "[OperadoresService.IniciarOperadoresAsync] " + + "Solicitação ignorada: o serviço está encerrando." + ); + + return false; } + DateTime agoraUtc = DateTime.UtcNow; + + lock (_estadoLock) + { + ultimoRestartUtc = agoraUtc; + Conectado = false; + TodosWorkersConectados = false; + TempoDesconectado = 0; + desconectadoDesdeUtc = null; + recebeuPrimeiroHeartbeatValido = false; + } + + Variaveis.MostrarLog( + "[OperadoresService.IniciarOperadoresAsync] " + + $"Iniciando operadores. Origem={origem}, " + + $"InicializaçãoCompleta={inicializacaoCompleta}, " + + $"GerenciarPython={iniciarScript}." + ); + + Task inicializacaoWorkersTask = InicializarWorkersSeNecessarioAsync(inicializacaoCompleta); + + bool pythonOk = true; + if (iniciarScript) { - EncerrarPythonAtual("restart do núcleo"); - - pythonProcess = PythonService.RunScript( - Path.Combine("workers", "main_async.py"), - new string[] { }, - "workers_main_async" - ); - - if (pythonProcess == null) - { - Variaveis.MostrarLog("[OperadoresService.IniciarOperadoresAsync] Falha ao iniciar processo Python."); - } - else - { - Variaveis.MostrarLog($"[OperadoresService.IniciarOperadoresAsync] Processo Python iniciado. PID={pythonProcess.Id}"); - } + pythonOk = ReiniciarProcessoPython(origem); } - - bool workersOk = true; - - if (tasksWorkers.Any()) + else { - bool[] resultados = await Task.WhenAll(tasksWorkers); - workersOk = resultados.Any(x => x); - - if (workersOk) - workersInicializados = true; + lock (_estadoLock) + { + PythonRodando = true; + inicioProcessoPythonUtc = agoraUtc; + } } + bool workersOk = await inicializacaoWorkersTask; + AtualizarEstadoProcessoPython(); - return (!iniciarScript || PythonRodando) && workersOk; + bool resultado = (!iniciarScript || (pythonOk && PythonRodando)) && workersOk; + + Variaveis.MostrarLog( + "[OperadoresService.IniciarOperadoresAsync] " + + $"Inicialização concluída. Resultado={(resultado ? "OK" : "PENDENTE")}, " + + $"Python={(PythonRodando ? "OK" : "OFF")}, " + + $"WorkersCSharp={(workersOk ? "OK" : "PENDENTE")}." + ); + + return resultado; } catch (Exception ex) { - Variaveis.MostrarLog("[OperadoresService.IniciarOperadoresAsync] Erro ao iniciar operadores: " + ex); + Variaveis.MostrarLog( + "[OperadoresService.IniciarOperadoresAsync] " + + "Erro ao iniciar operadores: " + ex + ); + return false; } finally { if (entrou) - _restartGate.Release(); + _cicloVidaGate.Release(); } } + private async Task InicializarWorkersSeNecessarioAsync(bool inicializacaoCompleta) + { + bool deveInicializar; + + lock (_estadoLock) + { + deveInicializar = inicializacaoCompleta || !workersInicializados; + } + + if (!deveInicializar) + return true; + + List> tarefas = Workers + .Where(x => x != null) + .Select(IniciarWorkerSeguroAsync) + .ToList(); + + if (tarefas.Count == 0) + { + lock (_estadoLock) + { + workersInicializados = false; + } + + Variaveis.MostrarLog( + "[OperadoresService.InicializarWorkersSeNecessarioAsync] " + + "Nenhum worker C# foi configurado." + ); + + return false; + } + + bool[] resultados = await Task.WhenAll(tarefas); + + // Todos os workers desta lista são críticos para a operação. + bool todosOk = resultados.All(x => x); + + lock (_estadoLock) + { + workersInicializados = todosOk; + } + + return todosOk; + } + private async Task IniciarWorkerSeguroAsync(IWorkerModel worker) { if (worker == null) @@ -157,179 +252,404 @@ namespace AgroBase.Services.Operadores try { - Variaveis.MostrarLog($"[OperadoresService.IniciarWorkerSeguroAsync] Inicializando worker C#: {nome}"); + Variaveis.MostrarLog( + "[OperadoresService.IniciarWorkerSeguroAsync] " + + $"Inicializando worker C#: {nome}." + ); - bool ok = await worker.IniciarProcessamento(); + Task tarefaInicializacao = worker.IniciarProcessamento(); + Task tarefaTimeout = Task.Delay(TimeoutIniciarWorkerMs); - Variaveis.MostrarLog($"[OperadoresService.IniciarWorkerSeguroAsync] Worker {nome} inicialização={(ok ? "OK" : "PENDENTE")}"); + Task concluida = await Task.WhenAny(tarefaInicializacao, tarefaTimeout); + + if (concluida != tarefaInicializacao) + { + tarefaInicializacao.ContinueWith( + tarefa => + { + Exception ignorada = tarefa.Exception; + }, + TaskContinuationOptions.OnlyOnFaulted + ); + + Variaveis.MostrarLog( + "[OperadoresService.IniciarWorkerSeguroAsync] " + + $"Timeout ao inicializar {nome} após " + + $"{TimeoutIniciarWorkerMs} ms." + ); + + return false; + } + + bool ok = await tarefaInicializacao; + + Variaveis.MostrarLog( + "[OperadoresService.IniciarWorkerSeguroAsync] " + + $"Worker {nome}: {(ok ? "OK" : "PENDENTE")}." + ); return ok; } catch (Exception ex) { - Variaveis.MostrarLog($"[OperadoresService.IniciarWorkerSeguroAsync] Erro ao iniciar worker {nome}: {ex.Message}"); + Variaveis.MostrarLog( + "[OperadoresService.IniciarWorkerSeguroAsync] " + + $"Erro ao inicializar {nome}: {ex}" + ); + return false; } } - public bool Encerrar() + private bool ReiniciarProcessoPython(string origem) { - encerrando = true; + EncerrarPythonAtual("reinicialização do núcleo; origem=" + origem); + + if (EstaEncerrando()) + return false; + + Process novoProcesso = PythonService.RunScript( + Path.Combine("workers", "main_async.py"), + new string[] { }, + "workers_main_async" + ); + + if (novoProcesso == null) + { + lock (_estadoLock) + { + pythonProcess = null; + PythonRodando = false; + inicioProcessoPythonUtc = DateTime.MinValue; + } + + Variaveis.MostrarLog( + "[OperadoresService.ReiniciarProcessoPython] " + + "Falha ao iniciar o processo Python." + ); + + return false; + } + + int pid; try { - tmrHearthbeat?.Stop(); + pid = novoProcesso.Id; + } + catch + { + pid = -1; } - catch { } - EncerrarPythonAtual("encerramento solicitado"); + lock (_estadoLock) + { + pythonProcess = novoProcesso; + PythonRodando = true; + inicioProcessoPythonUtc = DateTime.UtcNow; + desconectadoDesdeUtc = null; + recebeuPrimeiroHeartbeatValido = false; + TempoDesconectado = 0; + } - Conectado = false; - TodosWorkersConectados = false; - PythonRodando = false; - TempoDesconectado = 0; - - Variaveis.MostrarLog("[OperadoresService.Encerrar] Núcleo central encerrado."); + Variaveis.MostrarLog( + "[OperadoresService.ReiniciarProcessoPython] " + + $"Processo Python iniciado. PID={pid}, " + + $"GracePeriod={GracePeriodInicializacao.TotalSeconds:F0}s." + ); return true; } - private void EncerrarPythonAtual(string motivo = "não informado") + private void ReiniciarTimerHeartbeat() { try { - if (pythonProcess != null) + if (tmrHearthbeat != null) { - int pid = -1; - - try - { - pid = pythonProcess.Id; - } - catch { } - - Variaveis.MostrarLog($"[OperadoresService] Encerrando processo Python. PID={pid}, Motivo={motivo}"); - - encerrandoPythonIntencionalmente = true; - - PythonService.EncerrarProcesso(pythonProcess, matarArvore: true); - - Variaveis.MostrarLog($"[OperadoresService] Processo Python encerrado intencionalmente. PID={pid}, Motivo={motivo}"); + tmrHearthbeat.Stop(); + tmrHearthbeat.Dispose(); } } catch (Exception ex) { - Variaveis.MostrarLog("[OperadoresService] Erro ao encerrar Python atual: " + ex.Message); - } - finally - { - encerrandoPythonIntencionalmente = false; - pythonProcess = null; - PythonRodando = false; + Variaveis.MostrarLog( + "[OperadoresService.ReiniciarTimerHeartbeat] " + + "Erro ao descartar timer anterior: " + ex.Message + ); } + + tmrHearthbeat = new AsyncTaskTimerModel( + "tmrHearthbeatOperadores", + tmrHearthbeat_Tick, + IntervaloHeartbeatMs + ); + + tmrHearthbeat.Start(); } + #endregion + + #region Heartbeat + private async Task tmrHearthbeat_Tick() { - if (encerrando) + if (EstaEncerrando()) return; - var op = Variaveis.OperacaoEmAndamento; + bool entrou = false; try { + // Impede sobreposição caso uma leitura demore mais que o intervalo do timer. + entrou = await _heartbeatGate.WaitAsync(0); + + if (!entrou || EstaEncerrando()) + return; + + OperacaoModel op = Variaveis.OperacaoEmAndamento; + DateTime agoraUtc = DateTime.UtcNow; + AtualizarEstadoProcessoPython(); Dictionary statusWorkers = AtualizarSaudeWorkers(); - bool algumWorkerConectado = statusWorkers.Any(x => x.Value); - bool todosWorkersConectados = statusWorkers.Any() && statusWorkers.All(x => x.Value); + bool existemWorkers = statusWorkers.Count > 0; + bool todosWorkersOk = existemWorkers && statusWorkers.All(x => x.Value); - Conectado = iniciarScript - ? PythonRodando && algumWorkerConectado - : algumWorkerConectado; + bool pythonOk = !iniciarScript || PythonRodando; - TodosWorkersConectados = todosWorkersConectados; + bool nucleoCriticoOk = pythonOk && todosWorkersOk; - bool nucleoCriticoOk = Conectado; - - if (!nucleoCriticoOk) + lock (_estadoLock) { - TempoDesconectado++; - - AplicarParadaSeguraSeNecessario(op); - - RedisService.DefinirEquipamentoDesconectado(); - - LogarDiagnosticoDesconectado(statusWorkers); - - if (DeveTentarRestart()) - { - AudioAlertaService.Falar("Núcleo central de processamento desconectado, tentando reconectar."); - - await IniciarOperadoresAsync("heartbeat", inicializacaoCompleta: false); - } + TodosWorkersConectados = todosWorkersOk; + Conectado = nucleoCriticoOk; + } + if (nucleoCriticoOk) + { + TratarHeartbeatValido(op); return; } - if (TempoDesconectado > 0) - { - Variaveis.MostrarLog("[OperadoresService.tmrHearthbeat_Tick] Núcleo central reconectado."); - } + TratarHeartbeatInvalido(op, statusWorkers, agoraUtc); - TempoDesconectado = 0; + if (EstaEmGracePeriod(agoraUtc)) + return; - if (!(op?.Sensoriamento?.Operacao?.OperacaoConfigurada ?? false)) - { - HealthWorkerService.AtualizarDadosOperacao(true); - } + if (!DeveTentarRestart(agoraUtc)) + return; + + AudioAlertaService.Falar("Núcleo central de processamento desconectado, tentando reconectar."); + + await IniciarOperadoresAsync("heartbeat", inicializacaoCompleta: false); } catch (Exception ex) { - Variaveis.MostrarLog("[OperadoresService.tmrHearthbeat_Tick] Erro no heartbeat: " + ex); + Variaveis.MostrarLog( + "[OperadoresService.tmrHearthbeat_Tick] " + + "Erro no heartbeat: " + ex + ); + } + finally + { + if (entrou) + _heartbeatGate.Release(); } } + private void TratarHeartbeatValido(OperacaoModel op) + { + bool estavaDesconectado; + bool primeiroHeartbeat; + + lock (_estadoLock) + { + estavaDesconectado = desconectadoDesdeUtc.HasValue; + primeiroHeartbeat = !recebeuPrimeiroHeartbeatValido; + + recebeuPrimeiroHeartbeatValido = true; + desconectadoDesdeUtc = null; + TempoDesconectado = 0; + } + + if (primeiroHeartbeat) + { + Variaveis.MostrarLog( + "[OperadoresService.TratarHeartbeatValido] " + + "Primeiro heartbeat completo recebido. Núcleo pronto." + ); + } + else if (estavaDesconectado) + { + Variaveis.MostrarLog( + "[OperadoresService.TratarHeartbeatValido] " + + "Núcleo central reconectado." + ); + } + + if (!(op?.Sensoriamento?.Operacao?.OperacaoConfigurada ?? false)) + { + HealthWorkerService.AtualizarDadosOperacao(true); + } + } + + private void TratarHeartbeatInvalido(OperacaoModel op, Dictionary statusWorkers, DateTime agoraUtc) + { + lock (_estadoLock) + { + if (!desconectadoDesdeUtc.HasValue) + desconectadoDesdeUtc = agoraUtc; + + TempoDesconectado = Math.Max(0, (int)Math.Floor((agoraUtc - desconectadoDesdeUtc.Value).TotalSeconds)); + } + + // A segurança da operação é imediata. O grace period protege somente + // o processo contra restart prematuro. + AplicarParadaSeguraSeNecessario(op); + RedisService.DefinirEquipamentoDesconectado(); + + LogarDiagnosticoDesconectado(statusWorkers, agoraUtc); + } + + private bool EstaEmGracePeriod(DateTime agoraUtc) + { + DateTime inicioUtc; + bool primeiroHeartbeatRecebido; + + lock (_estadoLock) + { + inicioUtc = inicioProcessoPythonUtc; + primeiroHeartbeatRecebido = recebeuPrimeiroHeartbeatValido; + } + + if (primeiroHeartbeatRecebido) + return false; + + if (inicioUtc == DateTime.MinValue) + return false; + + return (agoraUtc - inicioUtc) < GracePeriodInicializacao; + } + + private bool DeveTentarRestart(DateTime agoraUtc) + { + DateTime ultimoRestartLocal; + DateTime? desconectadoDesdeLocal; + + lock (_estadoLock) + { + if (!iniciarScript || encerrando) + return false; + + ultimoRestartLocal = ultimoRestartUtc; + desconectadoDesdeLocal = desconectadoDesdeUtc; + } + + if (!desconectadoDesdeLocal.HasValue) + return false; + + TimeSpan tempoDesconectado = agoraUtc - desconectadoDesdeLocal.Value; + + if (tempoDesconectado < TimeSpan.FromSeconds(TimeoutDesconexaoSegundos)) + { + return false; + } + + if ((agoraUtc - ultimoRestartLocal) < IntervaloMinimoRestart) + { + return false; + } + + return true; + } + + #endregion + + #region Estado dos processos e workers + private void AtualizarEstadoProcessoPython() { - try + if (!iniciarScript) { - if (!iniciarScript) + lock (_estadoLock) { PythonRodando = true; - return; } - if (pythonProcess == null) + return; + } + + Process processo; + bool encerramentoIntencional; + + lock (_estadoLock) + { + processo = pythonProcess; + encerramentoIntencional = encerrandoPythonIntencionalmente; + } + + if (processo == null) + { + lock (_estadoLock) { PythonRodando = false; - return; } - if (pythonProcess.HasExited) - { - int exitCode = -999; + return; + } - try + try + { + if (!processo.HasExited) + { + lock (_estadoLock) { - exitCode = pythonProcess.ExitCode; + PythonRodando = true; } - catch { } - - PythonRodando = false; - - Variaveis.MostrarLog($"[OperadoresService.AtualizarEstadoProcessoPython] Processo Python não está rodando. ExitCode={exitCode}"); return; } - PythonRodando = true; + int exitCode; + + try + { + exitCode = processo.ExitCode; + } + catch + { + exitCode = -999; + } + + lock (_estadoLock) + { + PythonRodando = false; + } + + if (!encerramentoIntencional && !EstaEncerrando()) + { + Variaveis.MostrarLog( + "[OperadoresService.AtualizarEstadoProcessoPython] " + + $"Processo Python finalizou inesperadamente. ExitCode={exitCode}." + ); + } } catch (Exception ex) { - PythonRodando = false; - Variaveis.MostrarLog("[OperadoresService.AtualizarEstadoProcessoPython] Erro ao consultar processo Python: " + ex.Message); + lock (_estadoLock) + { + PythonRodando = false; + } + + if (!EstaEncerrando()) + { + Variaveis.MostrarLog( + "[OperadoresService.AtualizarEstadoProcessoPython] " + + "Erro ao consultar o processo Python: " + ex.Message + ); + } } } @@ -337,7 +657,7 @@ namespace AgroBase.Services.Operadores { Dictionary status = new Dictionary(); - foreach (var worker in Workers) + foreach (IWorkerModel worker in Workers) { if (worker == null) continue; @@ -348,20 +668,26 @@ namespace AgroBase.Services.Operadores { worker.AtualizarSaude(); - bool conectado = worker.Saude != null && worker.Saude.conectado; - - status[nome] = conectado; + status[nome] = worker.Saude != null && worker.Saude.conectado; } catch (Exception ex) { status[nome] = false; - Variaveis.MostrarLog($"[OperadoresService.AtualizarSaudeWorkers] Erro ao atualizar saúde do worker {nome}: {ex.Message}"); + + Variaveis.MostrarLog( + "[OperadoresService.AtualizarSaudeWorkers] " + + $"Erro ao atualizar a saúde de {nome}: {ex.Message}" + ); } } return status; } + #endregion + + #region Parada segura + private void AplicarParadaSeguraSeNecessario(OperacaoModel op) { try @@ -392,42 +718,43 @@ namespace AgroBase.Services.Operadores if (controle == null) return; - if (op.Parametros.Controle.MovimentoAutomatico) + if (op.Parametros.Controle.MovimentoAutomatico && controle.PercentualVelocidadeSP != 0) { - if (controle.PercentualVelocidadeSP != 0) - { - controle.PercentualVelocidadeSP = 0; + controle.PercentualVelocidadeSP = 0; - RedisService.AtualizarCampos( - CtxKey.DadosControle, - ("velocidade_sp", controle.PercentualVelocidadeSP) - ); - } + RedisService.AtualizarCampos( + CtxKey.DadosControle, + ("velocidade_sp", controle.PercentualVelocidadeSP) + ); } - if (op.Parametros.Controle.DirecionalAutomatico) + if (op.Parametros.Controle.DirecionalAutomatico && controle.Angulo != 0) { - if (controle.Angulo != 0) - { - controle.Angulo = 0; + controle.Angulo = 0; - RedisService.AtualizarCampos( - CtxKey.DadosControle, - ("angulo_sp", controle.Angulo) - ); - } + RedisService.AtualizarCampos( + CtxKey.DadosControle, + ("angulo_sp", controle.Angulo) + ); } if (op.Parametros.Controle.PulverizadorAutomatico) { var bicos = op.DispAtu?.Dados?.BicosPulverizadores ?? new List(); - if (bicos != null && bicos.Any(x => x.ComandoAtuar)) + + if (bicos.Any(x => x.ComandoAtuar)) { bicos.ForEach(x => x.ComandoAtuar = false); RedisService.AtualizarCampos( CtxKey.DadosControle, - ("controle_bicos", bicos.ToDictionary(x => x.Posicao - 1, x => x.ComandoAtuar)) + ( + "controle_bicos", + bicos.ToDictionary( + x => x.Posicao - 1, + x => x.ComandoAtuar + ) + ) ); } } @@ -436,48 +763,216 @@ namespace AgroBase.Services.Operadores } catch (Exception ex) { - Variaveis.MostrarLog("[OperadoresService.AplicarParadaSeguraSeNecessario] Erro ao aplicar parada segura: " + ex.Message); + Variaveis.MostrarLog( + "[OperadoresService.AplicarParadaSeguraSeNecessario] " + + "Erro ao aplicar parada segura: " + ex.Message + ); } } - private bool DeveTentarRestart() + #endregion + + #region Encerramento + + public bool Encerrar() { - if (!iniciarScript) - return false; + lock (_estadoLock) + { + if (encerrando) + return true; - if (TempoDesconectado < TimeoutDesconexao) - return false; + encerrando = true; + } - if ((DateTime.Now - ultimoRestart) < IntervaloMinimoRestart) - return false; + try + { + if (tmrHearthbeat != null) + { + tmrHearthbeat.Stop(); + tmrHearthbeat.Dispose(); + tmrHearthbeat = null; + } + } + catch (Exception ex) + { + Variaveis.MostrarLog( + "[OperadoresService.Encerrar] " + + "Erro ao encerrar timer de heartbeat: " + ex.Message + ); + } + + EncerrarPythonAtual("encerramento solicitado"); + + lock (_estadoLock) + { + Conectado = false; + TodosWorkersConectados = false; + PythonRodando = false; + TempoDesconectado = 0; + desconectadoDesdeUtc = null; + recebeuPrimeiroHeartbeatValido = false; + inicioProcessoPythonUtc = DateTime.MinValue; + } + + Variaveis.MostrarLog( + "[OperadoresService.Encerrar] " + + "Núcleo central encerrado." + ); return true; } - private void LogarDiagnosticoDesconectado(Dictionary statusWorkers) + private void EncerrarPythonAtual(string motivo) { - if ((DateTime.Now - ultimoLogDesconectado) < IntervaloLogDesconectado) - return; + Process processo; - ultimoLogDesconectado = DateTime.Now; - - string pythonStatus = iniciarScript - ? $"PythonRodando={PythonRodando}" - : "Python gerenciado externamente"; - - string workersStatus = string.Join( - ", ", - statusWorkers.Select(x => x.Key + "=" + (x.Value ? "OK" : "OFF")) - ); - - Variaveis.MostrarLog( - $"[OperadoresService.LogarDiagnosticoDesconectado] Núcleo desconectado há {TempoDesconectado}s. {pythonStatus}. Workers: {workersStatus}" - ); - - if (pythonProcess == null && iniciarScript) + lock (_estadoLock) { - Variaveis.MostrarLog("[OperadoresService.LogarDiagnosticoDesconectado] pythonProcess está null."); + processo = pythonProcess; + + if (processo == null) + { + PythonRodando = false; + return; + } + + encerrandoPythonIntencionalmente = true; + } + + int pid; + + try + { + pid = processo.Id; + } + catch + { + pid = -1; + } + + try + { + Variaveis.MostrarLog( + "[OperadoresService.EncerrarPythonAtual] " + + $"Encerrando processo Python. PID={pid}, Motivo={motivo}." + ); + + PythonService.EncerrarProcesso( + processo, + matarArvore: true + ); + + Variaveis.MostrarLog( + "[OperadoresService.EncerrarPythonAtual] " + + $"Processo Python encerrado intencionalmente. PID={pid}." + ); + } + catch (Exception ex) + { + Variaveis.MostrarLog( + "[OperadoresService.EncerrarPythonAtual] " + + $"Erro ao encerrar processo Python PID={pid}: {ex.Message}" + ); + } + finally + { + try + { + processo.Dispose(); + } + catch + { + // O encerramento do processo já foi solicitado. + } + + lock (_estadoLock) + { + if (ReferenceEquals(pythonProcess, processo)) + pythonProcess = null; + + PythonRodando = false; + encerrandoPythonIntencionalmente = false; + } } } + + private bool EstaEncerrando() + { + lock (_estadoLock) + { + return encerrando; + } + } + + #endregion + + #region Diagnóstico + + private void LogarDiagnosticoDesconectado(Dictionary statusWorkers, DateTime agoraUtc) + { + DateTime ultimoLogUtc; + DateTime inicioPythonUtc; + DateTime? desconectadoDesdeLocal; + bool primeiroHeartbeat; + bool pythonRodandoLocal; + int pid = -1; + + lock (_estadoLock) + { + ultimoLogUtc = ultimoLogDesconectadoUtc; + inicioPythonUtc = inicioProcessoPythonUtc; + desconectadoDesdeLocal = desconectadoDesdeUtc; + primeiroHeartbeat = recebeuPrimeiroHeartbeatValido; + pythonRodandoLocal = PythonRodando; + + if ((agoraUtc - ultimoLogUtc) < IntervaloLogDesconectado) + { + return; + } + + ultimoLogDesconectadoUtc = agoraUtc; + + if (pythonProcess != null) + { + try + { + pid = pythonProcess.Id; + } + catch + { + pid = -1; + } + } + } + + double idadeProcesso = inicioPythonUtc == DateTime.MinValue ? -1 : Math.Max(0, (agoraUtc - inicioPythonUtc).TotalSeconds); + double tempoDesconectado = desconectadoDesdeLocal.HasValue ? Math.Max(0, (agoraUtc - desconectadoDesdeLocal.Value).TotalSeconds) : 0; + double graceRestante = inicioPythonUtc == DateTime.MinValue || primeiroHeartbeat ? 0 : Math.Max(0, (GracePeriodInicializacao - (agoraUtc - inicioPythonUtc)).TotalSeconds); + + string workersStatus = statusWorkers.Count == 0 + ? "nenhum worker encontrado" + : string.Join( + ", ", + statusWorkers.Select( + x => x.Key + "=" + (x.Value ? "OK" : "OFF") + ) + ); + + string pythonStatus = iniciarScript + ? $"PID={pid}, Rodando={pythonRodandoLocal}, " + + $"Idade={idadeProcesso:F1}s" + : "gerenciado externamente"; + + Variaveis.MostrarLog( + "[OperadoresService.LogarDiagnosticoDesconectado] " + + $"Núcleo indisponível há {tempoDesconectado:F1}s. " + + $"Python=[{pythonStatus}]. " + + $"PrimeiroHeartbeat={primeiroHeartbeat}. " + + $"GraceRestante={graceRestante:F1}s. " + + $"Workers=[{workersStatus}]." + ); + } + + #endregion } } \ No newline at end of file