diff --git a/AgroBase/AgroBase/Forms/frmInstancial.cs b/AgroBase/AgroBase/Forms/frmInstancial.cs index 31f1fadb8..9ab40afd4 100644 --- a/AgroBase/AgroBase/Forms/frmInstancial.cs +++ b/AgroBase/AgroBase/Forms/frmInstancial.cs @@ -1,6 +1,7 @@ using AgroBase.Forms.IHM; using AgroBase.Models; using AgroBase.Services; +using AgroBase.Services.Operadores; using System; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,8 @@ namespace AgroBase.Forms { public partial class frmInstancial : Form { + public static readonly bool IniciarWorkers = true; + public static AsyncTaskTimerModel tmrVarreduraDispositivos; public static frmPrincipal frmPrincipal = new frmPrincipal(); @@ -21,11 +24,9 @@ namespace AgroBase.Forms //TopMost = true }; - private static readonly SemaphoreSlim _startupGate = - new SemaphoreSlim(1, 1); + private static readonly SemaphoreSlim _startupGate = new SemaphoreSlim(1, 1); - private static readonly SemaphoreSlim _shutdownGate = - new SemaphoreSlim(1, 1); + private static readonly SemaphoreSlim _shutdownGate = new SemaphoreSlim(1, 1); private static CancellationTokenSource _appCts; @@ -44,10 +45,7 @@ namespace AgroBase.Forms * O WinForms ainda está criando handles e a aplicação ainda não * possui lifecycle seguro para aguardar falhas. */ - ThreadPool.SetMinThreads( - workerThreads: 50, - completionPortThreads: 50 - ); + ThreadPool.SetMinThreads(workerThreads: 50, completionPortThreads: 50); } private async void frmInstancial_Load(object sender, EventArgs e) @@ -58,20 +56,11 @@ namespace AgroBase.Forms } catch (Exception ex) { - Variaveis.MostrarLog( - "[frmInstancial.Load] Falha crítica na inicialização: " + - ex - ); + Variaveis.MostrarLog("[frmInstancial.Load] Falha crítica na inicialização: " + ex); try { - MessageBox.Show( - "Falha ao inicializar o sistema:\n\n" + - ex.Message, - "AgroBase", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); + MessageBox.Show("Falha ao inicializar o sistema:\n\n" + ex.Message, "AgroBase", MessageBoxButtons.OK, MessageBoxIcon.Error); } catch { } } @@ -103,16 +92,13 @@ namespace AgroBase.Forms } catch (Exception ex) { - Variaveis.MostrarLog( - "[frmInstancial.Shown] " + ex - ); + Variaveis.MostrarLog("[frmInstancial.Shown] " + ex); } } private static async Task InicializarServicosAsync() { - await _startupGate.WaitAsync() - .ConfigureAwait(false); + await _startupGate.WaitAsync().ConfigureAwait(false); try { @@ -143,8 +129,7 @@ namespace AgroBase.Forms GeneralJoystick.IniciarRotinas(); - await Variaveis.IniciarMqttAsync(_appCts.Token) - .ConfigureAwait(false); + await Variaveis.IniciarMqttAsync(_appCts.Token).ConfigureAwait(false); /* * Hoje pode ficar comentado se UDP não for usado no rover, @@ -157,28 +142,19 @@ namespace AgroBase.Forms LivoxManagerProcess.Start(); - await VariaveisOperacao - .Operadores - .IniciarProcessamento(false) - .ConfigureAwait(false); + await VariaveisOperacao.Operadores.IniciarProcessamento(IniciarWorkers).ConfigureAwait(false); - AudioAlertaService.SelecionarVoz( - VariaveisEquipamento.VozAlerta - ); + AudioAlertaService.SelecionarVoz(VariaveisEquipamento.VozAlerta); IniciarTimerVarreduraDispositivos(); _startupConcluido = true; - Variaveis.MostrarLog( - "[frmInstancial] Inicialização concluída." - ); + Variaveis.MostrarLog("[frmInstancial] Inicialização concluída."); } catch { - await EncerrarProcessosInternoAsync( - sairProcesso: false - ).ConfigureAwait(false); + await EncerrarProcessosInternoAsync(sairProcesso: false).ConfigureAwait(false); throw; } @@ -210,29 +186,7 @@ namespace AgroBase.Forms if (Variaveis.Fechando) return; - await SerialService - .RealizarVarreduraPortasUSB() - .ConfigureAwait(false); - } - - /* - * Método mantido para compatibilidade com chamadas antigas. - * Não é mais chamado no Load, porque o startup correto é - * Variaveis.IniciarMqttAsync(). - */ - private async void IniciarConexaoMqtt() - { - try - { - await Variaveis.IniciarMqttAsync(); - } - catch (Exception ex) - { - Variaveis.MostrarLog( - "[frmInstancial.IniciarConexaoMqtt] " + - ex.Message - ); - } + await SerialService.RealizarVarreduraPortasUSB().ConfigureAwait(false); } public static Task EncerrarProcessos() @@ -376,6 +330,15 @@ namespace AgroBase.Forms Variaveis.MostrarLog("[frmInstancial.Encerrar] Erro ao encerrar MQTT: " + ex.Message); } + try + { + VariaveisOperacao.Operadores.Encerrar(); + } + catch (Exception ex) + { + Variaveis.MostrarLog("[frmInstancial.Encerrar] Erro ao encerrar OperadoresService: " + ex.Message); + } + try { _appCts?.Dispose(); diff --git a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs index 567bac357..fb418f5cb 100644 --- a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs +++ b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs @@ -1171,7 +1171,7 @@ namespace AgroBase.Models.Modules BicosPulverizadores = new List(), }; - for (int i = 0; i < Variaveis.OperacaoEmAndamento.Parametros.QtdBicos; i++) + for (int i = 0; i < VariaveisEquipamento.QuantidadeBicosPulverizadores; i++) { int posicao = (i + 1); Atuador.BicosPulverizadores.Add(new AtuadorBicoModel() @@ -1311,9 +1311,9 @@ namespace AgroBase.Models.Modules var op = Variaveis.OperacaoEmAndamento; var dadosAtuador = Dados; - var controle = op.Parametros?.Controle; + var controle = op?.Parametros?.Controle; - if (dadosAtuador == null || controle == null) + if (op == null || dadosAtuador == null || controle == null) return; var bomba = dadosAtuador.BombaPressurizadora; @@ -1420,7 +1420,7 @@ namespace AgroBase.Models.Modules if (!CanManager.CanService.IsConnected) return false; - if (op.DispAtu == null) return false; + if (op?.DispAtu == null) return false; CanManager.CanService.RegistrarHandler(_EnderecoCAN_Rx, _AtuHandler); @@ -1997,25 +1997,42 @@ namespace AgroBase.Models.Modules var op = Variaveis.OperacaoEmAndamento; List tasks = new List(); - var DispAtu = op.DispAtu; - if (DispAtu != null) - { - tasks.Add(DispAtu.Dados.RealizarTestesIniciais(forcar).ContinueWith(t => { - if (t.IsFaulted) - { - Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Erro no referenciamento do módulo {Modulo_ID}: {t.Exception?.InnerException?.Message}"); - op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Falha, 0, $"Erro no referenciamento do módulo {Modulo_ID}: {t.Exception?.InnerException?.Message}"); - } - if (t.IsCompleted) - { - var calAtu = op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo]; - var v = calAtu.Select(x => $"{x.Key} " + (x.Value ? "Sim" : "Não")); - Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Task de referenciamento do módulo {Modulo_ID} finalizada: " + string.Join(", ", v)); - op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Operante, 100, $"Task de referenciamento do módulo {Modulo_ID} finalizada: " + string.Join(", ", v)); - } - })); - } + if (op?.DispAtu?.Dados == null) + return tasks; + + tasks.Add(Task.Run(async () => + { + try + { + bool sucesso = await op.DispAtu.Dados.RealizarTestesIniciais(forcar); + + var calAtu = op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo]; + var v = calAtu.Select(x => $"{x.Key} " + (x.Value ? "Sim" : "Não")); + + if (sucesso) + { + Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Teste do módulo {Modulo_ID} finalizado com sucesso: " + string.Join(", ", v)); + + op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Operante, 100, $"Teste do módulo {Modulo_ID} finalizado com sucesso: " + string.Join(", ", v)); + } + else + { + Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v)); + + op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v)); + } + } + catch (Exception ex) + { + Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Erro no teste do módulo {Modulo_ID}: {ex.Message}"); + + op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Falha, 0, $"Erro no teste do módulo {Modulo_ID}: {ex.Message}"); + + throw; + } + })); + return tasks; } @@ -2685,7 +2702,7 @@ namespace AgroBase.Models.Modules valor.atual.valor = v; } - if (Posicao == CanMessagePosicaoDados.Status) + if (valor.funcao == FuncoesPinout.Iniciado && valor.posicao == CanMessagePosicaoDados.Status) { if (sensor != null) sensor.Inicializado = v; @@ -2861,7 +2878,7 @@ namespace AgroBase.Models.Modules malhaFechada, } ); - Variaveis.MostrarLog($"BOMBA {ID_Num}, INICIADO: {iniciado}, MALHA_FECHADA: {malhaFechada}"); + var bomba = Dados.BombasPressurizadoras?.FirstOrDefault(x => x.ID_Num == ID_Num); break; } case CanMessagePosicaoDados.Dados1: diff --git a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs index 67424fc7e..00a75f1a8 100644 --- a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs +++ b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs @@ -2810,7 +2810,7 @@ namespace AgroBase.Models.Modules valor.atual.valor = v; } - if (Posicao == CanMessagePosicaoDados.Status) + if (valor.funcao == FuncoesPinout.Iniciado && valor.posicao == CanMessagePosicaoDados.Status) { if (sensor != null) sensor.Inicializado = v; diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs index 0e51f0686..8f514ce65 100644 --- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs +++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs @@ -94,6 +94,7 @@ namespace AgroBase.Models private static string _discoverySessionId = Guid.NewGuid().ToString("N"); private static long _telemetrySequence = 0; + private readonly SemaphoreSlim _calibragemGate = new SemaphoreSlim(1, 1); @@ -1080,7 +1081,9 @@ namespace AgroBase.Models op.Sensoriamento.Operacao.OperacaoIniciada = false; op.Sensoriamento.Operacao.Emergencia = false; + op.Sensoriamento.Operacao.Calibrando = false; op.Sensoriamento.Operacao.Pausa = false; + RedisService.AtualizarCampos(CtxKey.DadosOperacao, ("calibrando", false)); op.Sensoriamento.Operacao.DataFim = DateTime.Now; @@ -1117,106 +1120,6 @@ namespace AgroBase.Models op.EnviarParametorsOperacao(); } - public async Task RealizarCalibragemInicialAsync(bool forcar) - { - var op = this; - - if (op.Sensoriamento.Operacao.Calibrando) return; - - var modsCalibragem = op.Sensoriamento.Operacao.ModsCalibragem; - - bool modsCalibrados = - modsCalibragem != null && - modsCalibragem.Any() && - modsCalibragem.All(x => - x.Value != null && - x.Value.Values.Count > 0 && - x.Value.Values.All(y => y == true) - ); - - if (!forcar && modsCalibrados) - { - op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Todos os módulos já foram calibrados anteriormente, não irá calibrar novamente."); - return; - } - - var DispAtu = op.DispAtu; - var DispMvd = op.DispMvd; - List sv = new List() { StatusModulo.Operante, StatusModulo.Alerta }; - var s = op.Sensoriamento.OperadorSaude.ModulosSaude; - var pControle = op.Parametros.Controle; - var _Controle = op.Controle; - - bool calibragemAtuNecessaria = pControle.PulverizadorAutomatico && (DispAtu?.Dados != null && sv.Contains(s.FirstOrDefault(x => x.modulo == T_Code.Atu)?.status ?? StatusModulo.Desconectado) && (forcar || modsCalibragem[T_Code.Atu].Any(x => !x.Value) || !modsCalibragem[T_Code.Atu].Any())); - bool calibragemDirNecessaria = (DispMvd?.Dados != null && sv.Contains(s.FirstOrDefault(x => x.modulo == T_Code.Dir)?.status ?? StatusModulo.Desconectado) && (forcar || modsCalibragem[T_Code.Dir].Any(x => !x.Value) || !modsCalibragem[T_Code.Dir].Any())); - bool calibragemMovNecessaria = (DispMvd?.Dados != null && sv.Contains(s.FirstOrDefault(x => x.modulo == T_Code.Mov)?.status ?? StatusModulo.Desconectado) && (forcar || modsCalibragem[T_Code.Mov].Any(x => !x.Value) || !modsCalibragem[T_Code.Mov].Any())); - - if (!calibragemAtuNecessaria && !calibragemDirNecessaria && !calibragemMovNecessaria) - { - if (forcar) - { - Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Calibragem não necessária"); - op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, $"Calibragem não necessária"); - } - return; - } - - Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Pausa para calibragem inicial"); - op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, $"Pausa para calibragem inicial"); - op.Sensoriamento.Operacao.Pausa = true; - await Task.Delay(5000); - - Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Calibragem dos modulos inciada..."); - op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, $"Calibragem dos modulos inciada..."); - op.Sensoriamento.Operacao.Calibrando = true; - RedisService.AtualizarCampos(CtxKey.DadosOperacao, ("calibrando", op.Sensoriamento.Operacao.Calibrando)); - - TipoMovimentoDirecional tipoControle = _Controle.TipoMovimento; - - var tasks = new List(); - - if (calibragemAtuNecessaria) tasks.AddRange(DispAtu.Dados.RealizarCalibragemAsync(forcar)); - if (calibragemDirNecessaria) tasks.AddRange(DispMvd.Dados.RealizarCalibragemDirAsync(forcar)); - if (calibragemMovNecessaria) tasks.AddRange(DispMvd.Dados.RealizarCalibragemMovAsync(forcar)); - - if (tasks.Count == 0) - { - Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Nenhuma calibragem foi iniciada."); - op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Alerta, 50, $"Nenhuma calibragem foi iniciada."); - _Controle.TipoMovimento = tipoControle; - op.Sensoriamento.Operacao.Calibrando = false; - op.Sensoriamento.Operacao.Pausa = false; - return; - } - - try - { - var allTasks = Task.WhenAll(tasks); - var completed = await Task.WhenAny(allTasks, Task.Delay(2 * 60 * 1000)); - - if (completed == allTasks) - { - Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Calibragem finalizada com sucesso!"); - op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, $"Calibragem finalizada com sucesso!"); - } - else - { - Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Timeout na calibragem. Continuando mesmo assim."); - op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Alerta, 50, $"Timeout na calibragem. Continuando mesmo assim."); - } - } - catch (Exception ex) - { - Variaveis.MostrarLog($"[OperacaoModel] [RealizarCalibragemInicialAsync] Erro durante calibragem: {ex.Message}"); - op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Falha, 0, $"Erro durante calibragem: {ex.Message}"); - } - _Controle.TipoMovimento = tipoControle; - op.Sensoriamento.Operacao.Calibrando = false; - op.Sensoriamento.Operacao.Pausa = false; - RedisService.AtualizarCampos(CtxKey.DadosOperacao, ("calibrando", op.Sensoriamento.Operacao.Calibrando)); - } - - public async Task IniciarSimulacao() { var op = this; @@ -1265,7 +1168,6 @@ namespace AgroBase.Models }); } - public void IniciarTreinamento() { var op = this; @@ -1362,6 +1264,220 @@ namespace AgroBase.Models } + #endregion + + #region CALIBRAGEM + + public async Task RealizarCalibragemInicialAsync(bool forcar) + { + var op = this; + + if (!ReferenceEquals(Variaveis.OperacaoEmAndamento, this)) + return; + + if (!await _calibragemGate.WaitAsync(0)) + return; + + TipoMovimentoDirecional tipoControle = op.Controle.TipoMovimento; + bool entrouEmCalibragem = false; + + try + { + GarantirEstruturaModsCalibragem(op); + + var modsCalibragem = op.Sensoriamento.Operacao.ModsCalibragem; + + bool modsCalibrados = + modsCalibragem.Any() && + modsCalibragem.All(x => + x.Value != null && + x.Value.Values.Count > 0 && + x.Value.Values.All(y => y) + ); + + if (!forcar && modsCalibrados) + { + //op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Todos os módulos já foram calibrados anteriormente, não irá calibrar novamente."); + return; + } + + var dispAtu = op.DispAtu; + var dispMvd = op.DispMvd; + + var statusValidos = new List() + { + StatusModulo.Operante, + StatusModulo.Alerta + }; + + var saude = op.Sensoriamento?.OperadorSaude?.ModulosSaude ?? new List(); + var pControle = op.Parametros?.Controle; + + if (pControle == null) + return; + + bool atuSaudavel = statusValidos.Contains(saude.FirstOrDefault(x => x.modulo == T_Code.Atu)?.status ?? StatusModulo.Desconectado); + bool dirSaudavel = statusValidos.Contains(saude.FirstOrDefault(x => x.modulo == T_Code.Dir)?.status ?? StatusModulo.Desconectado); + bool movSaudavel = statusValidos.Contains(saude.FirstOrDefault(x => x.modulo == T_Code.Mov)?.status ?? StatusModulo.Desconectado); + + bool reservatorioOk = (op.Sensoriamento?.Atuador?.PercentualReservatorio ?? 0) > VariaveisEquipamento.PercentualReservatorioMinCritio; + + bool bombaOk = dispAtu?.Dados?.BombaPressurizadora?.Inicializado ?? false; + + bool calibragemAtuNecessaria = + pControle.PulverizadorAutomatico && + bombaOk && + reservatorioOk && + dispAtu?.Dados != null && + atuSaudavel && + DeveCalibrar(modsCalibragem, T_Code.Atu, forcar); + + bool calibragemDirNecessaria = + dispMvd?.Dados != null && + dirSaudavel && + DeveCalibrar(modsCalibragem, T_Code.Dir, forcar); + + bool calibragemMovNecessaria = + dispMvd?.Dados != null && + movSaudavel && + DeveCalibrar(modsCalibragem, T_Code.Mov, forcar); + + if (!calibragemAtuNecessaria && !calibragemDirNecessaria && !calibragemMovNecessaria) + { + if (forcar) + { + string motivo = + $"Calibragem não necessária. " + + $"AtuNec={calibragemAtuNecessaria}, DirNec={calibragemDirNecessaria}, MovNec={calibragemMovNecessaria}, " + + $"PulvAuto={pControle.PulverizadorAutomatico}, BombaOk={bombaOk}, ReservatorioOk={reservatorioOk}, " + + $"AtuSaude={atuSaudavel}, DirSaude={dirSaudavel}, MovSaude={movSaudavel}"; + + Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] " + motivo); + + op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, motivo); + } + + return; + } + + entrouEmCalibragem = true; + + op.Sensoriamento.Operacao.Calibrando = true; + op.Sensoriamento.Operacao.Pausa = true; + + RedisService.AtualizarCampos(CtxKey.DadosOperacao, ("calibrando", true)); + + Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Pausa para calibragem inicial"); + + op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Pausa para calibragem inicial"); + + await Task.Delay(3000); + + if (!ReferenceEquals(Variaveis.OperacaoEmAndamento, this)) + return; + + if (!op.Sensoriamento.Operacao.OperacaoIniciada) + return; + + Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Calibragem dos módulos iniciada..."); + + op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem dos módulos iniciada..."); + + var tasks = new List(); + + if (calibragemAtuNecessaria) + tasks.AddRange(dispAtu.Dados.RealizarCalibragemAsync(forcar)); + + if (calibragemDirNecessaria) + tasks.AddRange(dispMvd.Dados.RealizarCalibragemDirAsync(forcar)); + + if (calibragemMovNecessaria) + tasks.AddRange(dispMvd.Dados.RealizarCalibragemMovAsync(forcar)); + + if (tasks.Count == 0) + { + Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Nenhuma calibragem foi iniciada."); + + op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Alerta, 50, "Nenhuma calibragem foi iniciada."); + + return; + } + + var allTasks = Task.WhenAll(tasks); + var timeoutTask = Task.Delay(2 * 60 * 1000); + + var completed = await Task.WhenAny(allTasks, timeoutTask); + + if (completed == allTasks) + { + await allTasks; + + Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Calibragem finalizada."); + + op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem finalizada."); + } + else + { + Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Timeout na calibragem. Liberando operação mesmo assim."); + + op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Alerta, 50, "Timeout na calibragem. Liberando operação mesmo assim."); + } + } + catch (Exception ex) + { + Variaveis.MostrarLog($"[OperacaoModel] [RealizarCalibragemInicialAsync] Erro durante calibragem: {ex}"); + + op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Falha, 0, $"Erro durante calibragem: {ex.Message}"); + } + finally + { + op.Controle.TipoMovimento = tipoControle; + + if (entrouEmCalibragem) + { + op.Sensoriamento.Operacao.Calibrando = false; + op.Sensoriamento.Operacao.Pausa = false; + + RedisService.AtualizarCampos(CtxKey.DadosOperacao, ("calibrando", false)); + } + + _calibragemGate.Release(); + } + } + + private static void GarantirEstruturaModsCalibragem(OperacaoModel op) + { + if (op.Sensoriamento.Operacao.ModsCalibragem == null) + op.Sensoriamento.Operacao.ModsCalibragem = new Dictionary>(); + + var mods = op.Sensoriamento.Operacao.ModsCalibragem; + + if (!mods.ContainsKey(T_Code.Mov) || mods[T_Code.Mov] == null) + mods[T_Code.Mov] = new Dictionary(); + + if (!mods.ContainsKey(T_Code.Dir) || mods[T_Code.Dir] == null) + mods[T_Code.Dir] = new Dictionary(); + + if (!mods.ContainsKey(T_Code.Atu) || mods[T_Code.Atu] == null) + mods[T_Code.Atu] = new Dictionary(); + } + + private static bool DeveCalibrar(Dictionary> modsCalibragem, T_Code modulo, bool forcar) + { + if (!modsCalibragem.ContainsKey(modulo) || modsCalibragem[modulo] == null) + return true; + + var mods = modsCalibragem[modulo]; + + if (!mods.Any()) + return true; + + if (forcar) + return true; + + return mods.Any(x => !x.Value); + } + #endregion #region CONTROLE DA OPERACAO @@ -2538,6 +2654,9 @@ namespace AgroBase.Models bool salvouComSucesso = false; DateTime agora = DateTime.Now; + DateTime novoCursorEventos = DateTime.Now; + List eventosSalvos = new List(); + try { if (string.IsNullOrWhiteSpace(op.ID)) @@ -2602,12 +2721,15 @@ namespace AgroBase.Models // Eventos discretos de saúde/erro // Melhor salvar um evento por linha. // ----------------------------- - if (sen.Logs != null && sen.Logs.Any()) + eventosSalvos = + (sen.Logs ?? new List()) + .Where(x => x != null) + .OrderBy(x => x.Momento) + .ToList(); + + foreach (var evento in eventosSalvos) { - foreach (var evento in sen.Logs.Where(x => x != null)) - { - SalvarLogLine("eventos", evento); - } + SalvarLogLine("eventos", evento); } // ----------------------------- @@ -2638,6 +2760,15 @@ namespace AgroBase.Models op.idxLog++; salvouComSucesso = true; + + if (eventosSalvos.Any()) + { + novoCursorEventos = eventosSalvos.Max(x => x.Momento); + } + else + { + novoCursorEventos = DateTime.Now; + } } catch (Exception ex) { @@ -2649,7 +2780,7 @@ namespace AgroBase.Models // Assim, se der erro no disco/JSON, você não perde eventos. if (salvouComSucesso && op?.Sensoriamento != null) { - op.Sensoriamento.UltimoRegistroLog = agora; + op.Sensoriamento.UltimoRegistroLog = novoCursorEventos; } try @@ -3323,7 +3454,7 @@ namespace AgroBase.Models } if (mod.saude_individual.Any()) { - foreach (var ind in mod.saude_individual) + foreach (var ind in mod.saude_individual.Where(x => x.em_uso)) { var ind_ant = saudeAnterior.FirstOrDefault(x => x.modulo == mod.modulo && x.saude_individual.Any(y => y.id == ind.id))?.saude_individual?.FirstOrDefault(x => x.id == ind.id); if (ind.status != ind_ant?.status) @@ -4376,18 +4507,37 @@ namespace AgroBase.Models public DateTime UltimoRegistroLog { get; set; } = DateTime.MinValue; + + private static readonly TimeSpan JanelaDedupeEvento = TimeSpan.FromSeconds(2); public void InserirLog(T_Code dispositivo, StatusModulo status, double saude, string mensagem, string id = null, string condicoes = null) { - if (op == null) return; + if (op == null) + return; try { if (Logs == null) Logs = new List(); + DateTime agora = DateTime.Now; + + bool duplicadoRecente = Logs.Any(x => + x != null && + (agora - x.Momento).Duration() <= JanelaDedupeEvento && + x.Dispositivo == dispositivo && + x.ID == id && + x.Status == status && + Math.Abs(x.Saude - saude) < 0.001 && + x.Mensagem == mensagem && + x.Condicoes == condicoes + ); + + if (duplicadoRecente) + return; + Logs.Add(new OperacaoSensoriamentoLogErrosModel() { - Momento = DateTime.Now, + Momento = agora, Dispositivo = dispositivo, ID = id, Status = status, @@ -4402,11 +4552,13 @@ namespace AgroBase.Models } } + public void AtualizarDados() { if (op == null) return; DateTime agora = DateTime.Now; + DateTime ultimoRegistroLog = UltimoRegistroLog; // Contexto HealthWorkerService.AtualizarDadosContexto(); @@ -4495,7 +4647,8 @@ namespace AgroBase.Models DadosPerformance = dadosPerformance, LivoxLidar = dadosLivox, CoolerControl = dadosCooler, - Logs = Logs != null ? Logs.Where(x => x?.Momento >= UltimoRegistroLog).ToList() : new List(), + Logs = Logs != null ? Logs.Where(x => x?.Momento >= ultimoRegistroLog).ToList() : new List(), + UltimoRegistroLog = ultimoRegistroLog, }; op.Sensoriamento = dadosAtualizados; diff --git a/AgroBase/AgroBase/Services/HerbicideCounterService.cs b/AgroBase/AgroBase/Services/HerbicideCounterService.cs index 598820713..ae7045a63 100644 --- a/AgroBase/AgroBase/Services/HerbicideCounterService.cs +++ b/AgroBase/AgroBase/Services/HerbicideCounterService.cs @@ -601,7 +601,7 @@ namespace AgroBase.Services { var op = Variaveis.OperacaoEmAndamento; - if (op.Sensoriamento.Operacao.Calibrando == true) + if (op?.Sensoriamento?.Operacao?.Calibrando == true) { /* * Durante a calibração, RealizarTestesIniciais() diff --git a/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs b/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs index 905eaba10..fd06e969a 100644 --- a/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs +++ b/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs @@ -34,7 +34,7 @@ namespace AgroBase.Services.Operadores DadosLeitura.Iniciado = true; bool pronto() => DadosLeitura.Pronto; - await FuncoesGlobais.AguardarCondicaoAsync(pronto, 5000); + await FuncoesGlobais.AguardarCondicaoAsync(pronto, VariaveisOperacao.Operadores.TimeoutIniciarWorkerMs); if (pronto()) { MostrarLog("Processamento iniciado com sucesso"); diff --git a/AgroBase/AgroBase/Services/Operadores/ManagerWorkerService.cs b/AgroBase/AgroBase/Services/Operadores/ManagerWorkerService.cs index cdd06d50e..e373792bd 100644 --- a/AgroBase/AgroBase/Services/Operadores/ManagerWorkerService.cs +++ b/AgroBase/AgroBase/Services/Operadores/ManagerWorkerService.cs @@ -35,7 +35,7 @@ namespace AgroBase.Services.Operadores DadosLeitura.Iniciado = true; bool pronto() => DadosLeitura.Pronto; - await FuncoesGlobais.AguardarCondicaoAsync(pronto, 5000); + await FuncoesGlobais.AguardarCondicaoAsync(pronto, VariaveisOperacao.Operadores.TimeoutIniciarWorkerMs); if (pronto()) { MostrarLog("Processamento iniciado com sucesso"); diff --git a/AgroBase/AgroBase/Services/Operadores/OperadoresService.cs b/AgroBase/AgroBase/Services/Operadores/OperadoresService.cs index 531b4536a..b3a339f33 100644 --- a/AgroBase/AgroBase/Services/Operadores/OperadoresService.cs +++ b/AgroBase/AgroBase/Services/Operadores/OperadoresService.cs @@ -1,8 +1,10 @@ using AgroBase.Models; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using static AgroBase.Models.Enums; using static AgroBase.Models.Operadores.OperadoresModels; @@ -11,144 +13,470 @@ namespace AgroBase.Services.Operadores { public class OperadoresService { + private readonly object _estadoLock = new object(); + private readonly SemaphoreSlim _restartGate = new SemaphoreSlim(1, 1); + private Process pythonProcess; public List Workers = new List() { new ManagerWorkerService(), - new VisualWorkerService(), + new VisualWorkerService(), new HealthWorkerService(), new CameraWorkerService(), new WeedWorkerService(), }; - private bool iniciarScript = false; + private bool iniciarScript = true; + private bool workersInicializados = false; + private bool encerrando = false; + private int TimeoutDesconexao = 3; - private double TempoDesconectado = 0; + 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 AsyncTaskTimerModel tmrHearthbeat; + public bool Conectado = false; + public bool TodosWorkersConectados = false; + public bool PythonRodando = false; + private bool encerrandoPythonIntencionalmente = false; 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(); - IniciarOperadores(); - - return true; - } - - private void IniciarOperadores() - { - foreach (var worker in Workers) + if (iniciou) { - worker.IniciarProcessamento(); + 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."); } - if (iniciarScript) + return iniciou; + } + + private async Task IniciarOperadoresAsync(string origem, bool inicializacaoCompleta) + { + bool entrou = false; + + try { - Encerrar(); - pythonProcess = PythonService.RunScript(Path.Combine("workers", "main_async.py"), new string[] { }); + entrou = await _restartGate.WaitAsync(0); + + if (!entrou) + { + Variaveis.MostrarLog("[OperadoresService.IniciarOperadoresAsync] Restart ignorado, já existe uma inicialização em andamento."); + return false; + } + + ultimoRestart = DateTime.Now; + + Variaveis.MostrarLog($"[OperadoresService.IniciarOperadoresAsync] Iniciando operadores. Origem={origem}, iniciarScript={iniciarScript}"); + + List> tasksWorkers = new List>(); + + if (!workersInicializados || inicializacaoCompleta) + { + foreach (var worker in Workers) + { + tasksWorkers.Add(IniciarWorkerSeguroAsync(worker)); + } + } + + 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}"); + } + } + + bool workersOk = true; + + if (tasksWorkers.Any()) + { + bool[] resultados = await Task.WhenAll(tasksWorkers); + workersOk = resultados.Any(x => x); + + if (workersOk) + workersInicializados = true; + } + + AtualizarEstadoProcessoPython(); + + return (!iniciarScript || PythonRodando) && workersOk; + } + catch (Exception ex) + { + Variaveis.MostrarLog("[OperadoresService.IniciarOperadoresAsync] Erro ao iniciar operadores: " + ex); + return false; + } + finally + { + if (entrou) + _restartGate.Release(); + } + } + + private async Task IniciarWorkerSeguroAsync(IWorkerModel worker) + { + if (worker == null) + return false; + + string nome = worker.GetType().Name; + + try + { + Variaveis.MostrarLog($"[OperadoresService.IniciarWorkerSeguroAsync] Inicializando worker C#: {nome}"); + + bool ok = await worker.IniciarProcessamento(); + + Variaveis.MostrarLog($"[OperadoresService.IniciarWorkerSeguroAsync] Worker {nome} inicialização={(ok ? "OK" : "PENDENTE")}"); + + return ok; + } + catch (Exception ex) + { + Variaveis.MostrarLog($"[OperadoresService.IniciarWorkerSeguroAsync] Erro ao iniciar worker {nome}: {ex.Message}"); + return false; } } public bool Encerrar() { - if (pythonProcess != null && !pythonProcess.HasExited) + encerrando = true; + + try { - pythonProcess.Kill(); + tmrHearthbeat?.Stop(); } - pythonProcess = null; - + catch { } + + EncerrarPythonAtual("encerramento solicitado"); + + Conectado = false; + TodosWorkersConectados = false; + PythonRodando = false; + TempoDesconectado = 0; + + Variaveis.MostrarLog("[OperadoresService.Encerrar] Núcleo central encerrado."); + return true; } + private void EncerrarPythonAtual(string motivo = "não informado") + { + try + { + if (pythonProcess != 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}"); + } + } + catch (Exception ex) + { + Variaveis.MostrarLog("[OperadoresService] Erro ao encerrar Python atual: " + ex.Message); + } + finally + { + encerrandoPythonIntencionalmente = false; + pythonProcess = null; + PythonRodando = false; + } + } + private async Task tmrHearthbeat_Tick() { + if (encerrando) + return; + var op = Variaveis.OperacaoEmAndamento; - Conectado = false; - foreach (var worker in Workers) + try { - worker.AtualizarSaude(); - Conectado |= worker.Saude.conectado; - } + AtualizarEstadoProcessoPython(); - if (!Conectado) - { - TempoDesconectado++; - if (op.Sensoriamento.Operacao.OperacaoIniciada && (op.Parametros.Controle.MovimentoAutomatico || op.Parametros.Controle.DirecionalAutomatico)) + Dictionary statusWorkers = AtualizarSaudeWorkers(); + + bool algumWorkerConectado = statusWorkers.Any(x => x.Value); + bool todosWorkersConectados = statusWorkers.Any() && statusWorkers.All(x => x.Value); + + Conectado = iniciarScript + ? PythonRodando && algumWorkerConectado + : algumWorkerConectado; + + TodosWorkersConectados = todosWorkersConectados; + + bool nucleoCriticoOk = Conectado; + + if (!nucleoCriticoOk) { - RedisService.AtualizarCampos( - CtxKey.DadosOperacao, - ("status", StatusOperacao.Parado), - ("liberado", false), - ("configurado", false) - ); + TempoDesconectado++; - var _Controle = op.Controle; - if (op.Parametros.Controle.MovimentoAutomatico) + AplicarParadaSeguraSeNecessario(op); + + RedisService.DefinirEquipamentoDesconectado(); + + LogarDiagnosticoDesconectado(statusWorkers); + + if (DeveTentarRestart()) { - if (_Controle.PercentualVelocidadeSP > 0) - { - _Controle.PercentualVelocidadeSP = 0; - RedisService.AtualizarCampos( - CtxKey.DadosControle, - ("velocidade_sp", _Controle.PercentualVelocidadeSP) - ); - } + AudioAlertaService.Falar("Núcleo central de processamento desconectado, tentando reconectar."); + + await IniciarOperadoresAsync("heartbeat", inicializacaoCompleta: false); } - if (op.Parametros.Controle.DirecionalAutomatico) - { - if (_Controle.Angulo != 0) - { - _Controle.Angulo = 0; - RedisService.AtualizarCampos( - CtxKey.DadosControle, - ("angulo_sp", _Controle.Angulo) - ); - } - } - - if (op.Parametros.Controle.PulverizadorAutomatico) - { - if (_Controle.Bicos.Any(x => x.ComandoAtuar)) - { - _Controle.Bicos.ForEach(x => x.ComandoAtuar = false); - RedisService.AtualizarCampos( - CtxKey.DadosControle, - ("controle_bicos", _Controle.Bicos.ToDictionary(x => x.Posicao - 1, x => x.ComandoAtuar)) - ); - } - } - - op.AtualizaInformacoesControleOperacao(); + return; } - RedisService.DefinirEquipamentoDesconectado(); - - // A cada 10 segundos desconectado, tenta reiniciar o nucleo de processamento - if (TempoDesconectado > 0 && TempoDesconectado % 10 == 0) + if (TempoDesconectado > 0) { - IniciarOperadores(); - AudioAlertaService.Falar("Núcleo central de processamento desconectado, tentando reconectar."); + Variaveis.MostrarLog("[OperadoresService.tmrHearthbeat_Tick] Núcleo central reconectado."); } - } - else - { + TempoDesconectado = 0; - if (!op.Sensoriamento.Operacao.OperacaoConfigurada) + + if (!(op?.Sensoriamento?.Operacao?.OperacaoConfigurada ?? false)) { HealthWorkerService.AtualizarDadosOperacao(true); } } + catch (Exception ex) + { + Variaveis.MostrarLog("[OperadoresService.tmrHearthbeat_Tick] Erro no heartbeat: " + ex); + } } + private void AtualizarEstadoProcessoPython() + { + try + { + if (!iniciarScript) + { + PythonRodando = true; + return; + } + if (pythonProcess == null) + { + PythonRodando = false; + return; + } + + if (pythonProcess.HasExited) + { + int exitCode = -999; + + try + { + exitCode = pythonProcess.ExitCode; + } + catch { } + + PythonRodando = false; + + Variaveis.MostrarLog($"[OperadoresService.AtualizarEstadoProcessoPython] Processo Python não está rodando. ExitCode={exitCode}"); + + return; + } + + PythonRodando = true; + } + catch (Exception ex) + { + PythonRodando = false; + Variaveis.MostrarLog("[OperadoresService.AtualizarEstadoProcessoPython] Erro ao consultar processo Python: " + ex.Message); + } + } + + private Dictionary AtualizarSaudeWorkers() + { + Dictionary status = new Dictionary(); + + foreach (var worker in Workers) + { + if (worker == null) + continue; + + string nome = worker.GetType().Name; + + try + { + worker.AtualizarSaude(); + + bool conectado = worker.Saude != null && worker.Saude.conectado; + + status[nome] = conectado; + } + catch (Exception ex) + { + status[nome] = false; + Variaveis.MostrarLog($"[OperadoresService.AtualizarSaudeWorkers] Erro ao atualizar saúde do worker {nome}: {ex.Message}"); + } + } + + return status; + } + + private void AplicarParadaSeguraSeNecessario(OperacaoModel op) + { + try + { + if (op?.Sensoriamento?.Operacao?.OperacaoIniciada != true) + return; + + if (op.Parametros?.Controle == null) + return; + + bool temControleAutomatico = + op.Parametros.Controle.MovimentoAutomatico || + op.Parametros.Controle.DirecionalAutomatico || + op.Parametros.Controle.PulverizadorAutomatico; + + if (!temControleAutomatico) + return; + + RedisService.AtualizarCampos( + CtxKey.DadosOperacao, + ("status", StatusOperacao.Parado), + ("liberado", false), + ("configurado", false) + ); + + var controle = op.Controle; + + if (controle == null) + return; + + if (op.Parametros.Controle.MovimentoAutomatico) + { + if (controle.PercentualVelocidadeSP != 0) + { + controle.PercentualVelocidadeSP = 0; + + RedisService.AtualizarCampos( + CtxKey.DadosControle, + ("velocidade_sp", controle.PercentualVelocidadeSP) + ); + } + } + + if (op.Parametros.Controle.DirecionalAutomatico) + { + if (controle.Angulo != 0) + { + controle.Angulo = 0; + + RedisService.AtualizarCampos( + CtxKey.DadosControle, + ("angulo_sp", controle.Angulo) + ); + } + } + + if (op.Parametros.Controle.PulverizadorAutomatico) + { + if (controle.Bicos != null && controle.Bicos.Any(x => x.ComandoAtuar)) + { + controle.Bicos.ForEach(x => x.ComandoAtuar = false); + + RedisService.AtualizarCampos( + CtxKey.DadosControle, + ("controle_bicos", controle.Bicos.ToDictionary(x => x.Posicao - 1, x => x.ComandoAtuar)) + ); + } + } + + op.AtualizaInformacoesControleOperacao(); + } + catch (Exception ex) + { + Variaveis.MostrarLog("[OperadoresService.AplicarParadaSeguraSeNecessario] Erro ao aplicar parada segura: " + ex.Message); + } + } + + private bool DeveTentarRestart() + { + if (!iniciarScript) + return false; + + if (TempoDesconectado < TimeoutDesconexao) + return false; + + if ((DateTime.Now - ultimoRestart) < IntervaloMinimoRestart) + return false; + + return true; + } + + private void LogarDiagnosticoDesconectado(Dictionary statusWorkers) + { + if ((DateTime.Now - ultimoLogDesconectado) < IntervaloLogDesconectado) + return; + + 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) + { + Variaveis.MostrarLog("[OperadoresService.LogarDiagnosticoDesconectado] pythonProcess está null."); + } + } } -} +} \ No newline at end of file diff --git a/AgroBase/AgroBase/Services/Operadores/VisualWorkerService.cs b/AgroBase/AgroBase/Services/Operadores/VisualWorkerService.cs index 73d755936..7f527aa4a 100644 --- a/AgroBase/AgroBase/Services/Operadores/VisualWorkerService.cs +++ b/AgroBase/AgroBase/Services/Operadores/VisualWorkerService.cs @@ -39,7 +39,7 @@ namespace AgroBase.Services.Operadores DadosLeitura.Iniciado = true; bool pronto() => DadosLeitura.Pronto; - await FuncoesGlobais.AguardarCondicaoAsync(pronto, 5000); + await FuncoesGlobais.AguardarCondicaoAsync(pronto, VariaveisOperacao.Operadores.TimeoutIniciarWorkerMs); if (pronto()) { MostrarLog("Processamento iniciado com sucesso"); diff --git a/AgroBase/AgroBase/Services/Operadores/WeedWorkerService.cs b/AgroBase/AgroBase/Services/Operadores/WeedWorkerService.cs index ae0cc8e2e..60e31c81c 100644 --- a/AgroBase/AgroBase/Services/Operadores/WeedWorkerService.cs +++ b/AgroBase/AgroBase/Services/Operadores/WeedWorkerService.cs @@ -37,7 +37,7 @@ namespace AgroBase.Services.Operadores DadosLeitura.Iniciado = true; bool pronto() => DadosLeitura.Pronto; - await FuncoesGlobais.AguardarCondicaoAsync(pronto, 5000); + await FuncoesGlobais.AguardarCondicaoAsync(pronto, VariaveisOperacao.Operadores.TimeoutIniciarWorkerMs); if (pronto()) { MostrarLog("Processamento iniciado com sucesso"); diff --git a/AgroBase/AgroBase/Services/PythonService.cs b/AgroBase/AgroBase/Services/PythonService.cs index a3c9c66d2..84a1edb8d 100644 --- a/AgroBase/AgroBase/Services/PythonService.cs +++ b/AgroBase/AgroBase/Services/PythonService.cs @@ -4,39 +4,65 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; +using System.Text; namespace AgroBase.Services { public class PythonService { public static bool DebugMode = false; - public static string PythonExe => Path.Combine(CaminhoGeral, "venv", "Scripts", "python.exe"); + public static string CaminhoGeral = "Python\\"; public static string CaminhoScripts = "Scripts\\"; public static string CaminhoLeitura = "Output\\"; - public static string ScriptWeedDetector + private static readonly object _lockProcessos = new object(); + private static readonly object _lockLogs = new object(); + + private static readonly List processosIniciados = new List(); + + public static string BaseDirectory { get { - return VersionamentoService.ArquivoScript(Enums.TipoArquivoVersionado.Script_WeedDetector).NomeArquivoLocal; + return AppDomain.CurrentDomain.BaseDirectory; } } - public static string ScriptGreenDetector + + public static string CaminhoPythonRoot { get { - return VersionamentoService.ArquivoScript(Enums.TipoArquivoVersionado.Script_WeedDetector).NomeArquivoLocal; + return ResolverCaminho(CaminhoGeral); } } - public static string ScriptStreetDetector + + public static string PythonExe { get { - return VersionamentoService.ArquivoScript(Enums.TipoArquivoVersionado.Script_StreetDetector).NomeArquivoLocal; + return Path.Combine(CaminhoPythonRoot, "venv", "Scripts", "python.exe"); } } + + public static string ScriptsRoot + { + get + { + return Path.Combine(CaminhoPythonRoot, CaminhoScripts); + } + } + + public static string LogsRoot + { + get + { + return Path.Combine(CaminhoPythonRoot, "Logs"); + } + } + public static string ScriptMapConverter { get @@ -44,6 +70,7 @@ namespace AgroBase.Services return VersionamentoService.ArquivoScript(Enums.TipoArquivoVersionado.Script_MapLoad).NomeArquivoLocal; } } + public static string ScriptMapFollower { get @@ -51,6 +78,7 @@ namespace AgroBase.Services return VersionamentoService.ArquivoScript(Enums.TipoArquivoVersionado.Script_MapFollow).NomeArquivoLocal; } } + public static string ScriptMapGPS { get @@ -58,13 +86,7 @@ namespace AgroBase.Services return VersionamentoService.ArquivoScript(Enums.TipoArquivoVersionado.Script_GpsViewer).NomeArquivoLocal; } } - public static string ScriptListOAK - { - get - { - return VersionamentoService.ArquivoScript(Enums.TipoArquivoVersionado.ScriptListOAKCaneras).NomeArquivoLocal; - } - } + public static string ScriptMPCController { get @@ -73,164 +95,423 @@ namespace AgroBase.Services } } - - private static List processosIniciados = new List(); - - public static Process RunScript(string Script, string[] Argumentos) + public static Process RunScript(string script, string[] argumentos) { + return RunScript(script, argumentos, "python"); + } + + public static Process RunScript(string script, string[] argumentos, string nomeLog) + { + string logPath = null; + try { - string ScriptPath = CaminhoGeral + CaminhoScripts + Script; - string Args = string.Join(" ", Argumentos.Select(arg => $"\"{(arg ?? "").Replace("\\", "/")}\"")); + Directory.CreateDirectory(LogsRoot); - Process pythonProcess = new Process + logPath = CriarCaminhoLog(nomeLog); + + string pythonExe = PythonExe; + string scriptPath = ResolverScriptPath(script); + + EscreverLog(logPath, "============================================================"); + EscreverLog(logPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Iniciando processo Python"); + EscreverLog(logPath, $"BaseDirectory: {BaseDirectory}"); + EscreverLog(logPath, $"CaminhoPythonRoot: {CaminhoPythonRoot}"); + EscreverLog(logPath, $"PythonExe: {pythonExe}"); + EscreverLog(logPath, $"ScriptPath: {scriptPath}"); + EscreverLog(logPath, $"WorkingDirectory: {BaseDirectory}"); + + if (!File.Exists(pythonExe)) { - StartInfo = new ProcessStartInfo - { - FileName = PythonExe, - Arguments = $"{ScriptPath} {Args} --source={Variaveis.NomeAplicacao}", - UseShellExecute = false, - RedirectStandardOutput = false, - RedirectStandardError = false, - CreateNoWindow = true, - Verb = Variaveis.NomeAplicacao - } - }; - - Console.WriteLine($"{pythonProcess.StartInfo.FileName} {pythonProcess.StartInfo.Arguments}"); - - //pythonProcess.Exited += PythonProcess_Exited; - //pythonProcess.OutputDataReceived += PythonProcess_OutputDataReceived; - //pythonProcess.ErrorDataReceived += PythonProcess_ErrorDataReceived; - - pythonProcess.Start(); - - //pythonProcess.BeginOutputReadLine(); - //pythonProcess.BeginErrorReadLine(); - - lock (processosIniciados) - { - processosIniciados.Add(pythonProcess); + string msg = $"Python não encontrado: {pythonExe}"; + EscreverLog(logPath, msg); + Console.WriteLine(msg); + return null; } - return pythonProcess; + if (!File.Exists(scriptPath)) + { + string msg = $"Script Python não encontrado: {scriptPath}"; + EscreverLog(logPath, msg); + Console.WriteLine(msg); + return null; + } + + string args = MontarArgumentos(scriptPath, argumentos); + + EscreverLog(logPath, $"Arguments: {args}"); + + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = pythonExe, + Arguments = args, + WorkingDirectory = BaseDirectory, + + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + startInfo.EnvironmentVariables["PYTHONUNBUFFERED"] = "1"; + startInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; + startInfo.EnvironmentVariables["AGROBASE_SOURCE"] = Variaveis.NomeAplicacao ?? ""; + + string pythonPathAtual = startInfo.EnvironmentVariables["PYTHONPATH"] ?? ""; + string pythonPathNovo = ScriptsRoot; + + string workersPath = Path.Combine(ScriptsRoot, "workers"); + if (Directory.Exists(workersPath)) + pythonPathNovo += Path.PathSeparator + workersPath; + + if (!string.IsNullOrWhiteSpace(pythonPathAtual)) + pythonPathNovo += Path.PathSeparator + pythonPathAtual; + + startInfo.EnvironmentVariables["PYTHONPATH"] = pythonPathNovo; + + Process processo = new Process + { + StartInfo = startInfo, + EnableRaisingEvents = true + }; + + processo.OutputDataReceived += (sender, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) + return; + + string linha = "[OUT] " + e.Data; + EscreverLog(logPath, linha); + + if (DebugMode) + Variaveis.MostrarLog($"[PythonService.RunScript] {linha}"); + }; + + processo.ErrorDataReceived += (sender, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) + return; + + string linha = "[ERR] " + e.Data; + EscreverLog(logPath, linha); + Variaveis.MostrarLog($"[PythonService.RunScript] {linha}"); + }; + + processo.Exited += (sender, e) => + { + try + { + int exitCode = -999; + + try + { + exitCode = processo.ExitCode; + } + catch { } + + EscreverLog(logPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Processo Python finalizado. PID={processo.Id}, ExitCode={exitCode}"); + + lock (_lockProcessos) + { + processosIniciados.Remove(processo); + } + } + catch { } + }; + + Variaveis.MostrarLog($"[PythonService.RunScript] {startInfo.FileName} {startInfo.Arguments}"); + + bool iniciou = processo.Start(); + + if (!iniciou) + { + EscreverLog(logPath, "Process.Start retornou false."); + return null; + } + + processo.BeginOutputReadLine(); + processo.BeginErrorReadLine(); + + lock (_lockProcessos) + { + processosIniciados.Add(processo); + } + + EscreverLog(logPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Processo iniciado com PID={processo.Id}"); + + return processo; } catch (Exception ex) { - Console.WriteLine($"Erro ao iniciar script Python: {ex.Message}"); + string msg = $"Erro ao iniciar script Python: {ex}"; + + Variaveis.MostrarLog($"[PythonService.RunScript] {msg}"); + + try + { + Directory.CreateDirectory(LogsRoot); + + if (string.IsNullOrWhiteSpace(logPath)) + logPath = Path.Combine(LogsRoot, "python_start_error.log"); + + EscreverLog(logPath, msg); + } + catch { } + return null; } } - public static async Task RunScriptWithTimeout(string Script, string[] Argumentos, int timeoutMs) + public static async Task RunScriptWithTimeout(string script, string[] argumentos, int timeoutMs) { - var processo = RunScript(Script, Argumentos); - if (processo == null) return false; + Process processo = RunScript(script, argumentos, "python_timeout"); + + if (processo == null) + return false; + + bool completed = await Task.Run(() => processo.WaitForExit(timeoutMs)); - var completed = await Task.Run(() => processo.WaitForExit(timeoutMs)); if (!completed) { - Console.WriteLine("Script Python excedeu o tempo limite e será encerrado."); - processo.Kill(); + Variaveis.MostrarLog("[PythonService.RunScriptWithTimeout] Script Python excedeu o tempo limite e será encerrado."); + EncerrarProcesso(processo, true); return false; } - return true; + return processo.ExitCode == 0; } - public static string RunScriptWithCallback(string Script, string[] Argumentos) + public static string RunScriptWithCallback(string script, string[] argumentos) { + string logPath = null; + try { - string ScriptPath = CaminhoGeral + CaminhoScripts + Script; - string Args = string.Join(" ", Argumentos.Select(arg => $"\"{arg.Replace("\\", "/")}\"")); + Directory.CreateDirectory(LogsRoot); - Process pythonProcess = new Process + logPath = CriarCaminhoLog("python_callback"); + + string pythonExe = PythonExe; + string scriptPath = ResolverScriptPath(script); + + if (!File.Exists(pythonExe)) + return $"Python não encontrado: {pythonExe}"; + + if (!File.Exists(scriptPath)) + return $"Script Python não encontrado: {scriptPath}"; + + string args = MontarArgumentos(scriptPath, argumentos); + + ProcessStartInfo startInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - FileName = PythonExe, - Arguments = $"{ScriptPath} {Args}", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - Verb = Variaveis.NomeAplicacao - } + FileName = pythonExe, + Arguments = args, + WorkingDirectory = BaseDirectory, + + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true }; - Console.WriteLine($"{pythonProcess.StartInfo.FileName} {pythonProcess.StartInfo.Arguments}"); + startInfo.EnvironmentVariables["PYTHONUNBUFFERED"] = "1"; + startInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; - pythonProcess.Start(); + using (Process processo = new Process()) + { + processo.StartInfo = startInfo; - string output = pythonProcess.StandardOutput.ReadToEnd(); - pythonProcess.WaitForExit(); + Variaveis.MostrarLog($"[PythonService.RunScriptWithTimeout] {startInfo.FileName} {startInfo.Arguments}"); - return output; + processo.Start(); + + string output = processo.StandardOutput.ReadToEnd(); + string error = processo.StandardError.ReadToEnd(); + + processo.WaitForExit(); + + string resultado = + output + + Environment.NewLine + + error + + Environment.NewLine + + $"ExitCode={processo.ExitCode}"; + + EscreverLog(logPath, resultado); + + return resultado; + } } catch (Exception ex) { - Console.WriteLine($"Erro ao iniciar script Python: {ex.Message}"); - return null; + string msg = $"Erro ao iniciar script Python: {ex}"; + Variaveis.MostrarLog($"[PythonService.RunScriptWithTimeout] {msg}"); + + if (!string.IsNullOrWhiteSpace(logPath)) + EscreverLog(logPath, msg); + + return msg; } } - private static void PythonProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e) + public static bool EncerrarProcesso(Process processo, bool matarArvore) { - if (!string.IsNullOrWhiteSpace(e.Data)) - { - Console.WriteLine($"Erro recebidos pelo Python: {e.Data}"); - // Log adicional ou notificação pode ser adicionado aqui - } - } + if (processo == null) + return true; - private static void PythonProcess_OutputDataReceived(object sender, DataReceivedEventArgs e) - { - if (DebugMode) + try { - Console.WriteLine("Dados recebidos pelo Python: " + e.Data); - } - } + if (processo.HasExited) + return true; - private static void PythonProcess_Exited(object sender, EventArgs e) - { - if (DebugMode) - { - Console.WriteLine("Processo Python finalizado"); - } + int pid = processo.Id; - lock (processosIniciados) + if (matarArvore) + { + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = "taskkill", + Arguments = $"/PID {pid} /T /F", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using (Process killer = Process.Start(psi)) + { + if (killer != null) + killer.WaitForExit(5000); + } + } + else + { + processo.Kill(); + processo.WaitForExit(5000); + } + + return true; + } + catch (Exception ex) { - var processo = (Process)sender; - processosIniciados.Remove(processo); - processo.Dispose(); + Variaveis.MostrarLog($"[PythonService.EncerrarProcesso] Erro ao encerrar processo Python: {ex.Message}"); + return false; + } + finally + { + lock (_lockProcessos) + { + processosIniciados.Remove(processo); + } } } public static void EncerrarProcessos(int processId = -1) { - return; - foreach (var processo in Process.GetProcessesByName("python")) + List processos; + + lock (_lockProcessos) + { + processos = processosIniciados + .Where(x => x != null) + .ToList(); + } + + foreach (Process processo in processos) { try { - // Verifica os argumentos do processo - string argumentos = processo.StartInfo.Arguments; - if ((processId > -1 && processo.Id == processId)) - { - processo.Kill(); - } - else if (processId == -1) - { - processo.Kill(); - } + if (processId > -1 && processo.Id != processId) + continue; + + EncerrarProcesso(processo, true); } catch (Exception ex) { - Console.WriteLine($"Erro ao encerrar processo: {ex.Message}"); + Variaveis.MostrarLog($"[PythonService.EncerrarProcessos] Erro ao encerrar processo controlado: {ex.Message}"); } } } + private static string ResolverCaminho(string caminho) + { + if (string.IsNullOrWhiteSpace(caminho)) + return BaseDirectory; + + if (Path.IsPathRooted(caminho)) + return Path.GetFullPath(caminho); + + return Path.GetFullPath(Path.Combine(BaseDirectory, caminho)); + } + + private static string ResolverScriptPath(string script) + { + if (string.IsNullOrWhiteSpace(script)) + return ""; + + if (Path.IsPathRooted(script)) + return Path.GetFullPath(script); + + return Path.GetFullPath(Path.Combine(ScriptsRoot, script)); + } + + private static string MontarArgumentos(string scriptPath, string[] argumentos) + { + List args = new List(); + + args.Add(QuoteArg(scriptPath)); + + if (argumentos != null) + { + foreach (string arg in argumentos) + args.Add(QuoteArg((arg ?? "").Replace("\\", "/"))); + } + + args.Add("--source=" + QuoteArg(Variaveis.NomeAplicacao ?? "")); + + return string.Join(" ", args); + } + + private static string QuoteArg(string value) + { + if (value == null) + value = ""; + + value = value.Replace("\"", "\\\""); + + return "\"" + value + "\""; + } + + private static string CriarCaminhoLog(string nomeLog) + { + string safeName = string.IsNullOrWhiteSpace(nomeLog) + ? "python" + : nomeLog.Replace(" ", "_").Replace("/", "_").Replace("\\", "_"); + + string nomeArquivo = safeName + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".log"; + + return Path.Combine(LogsRoot, nomeArquivo); + } + + private static void EscreverLog(string logPath, string texto) + { + try + { + if (string.IsNullOrWhiteSpace(logPath)) + return; + + lock (_lockLogs) + { + File.AppendAllText(logPath, texto + Environment.NewLine, Encoding.UTF8); + } + } + catch { } + } } -} +} \ No newline at end of file