diff --git a/AgroBase/AgroBase/Forms/Operacoes/frmOperacaoManual.cs b/AgroBase/AgroBase/Forms/Operacoes/frmOperacaoManual.cs index 27e89f21d..e951cae61 100644 --- a/AgroBase/AgroBase/Forms/Operacoes/frmOperacaoManual.cs +++ b/AgroBase/AgroBase/Forms/Operacoes/frmOperacaoManual.cs @@ -763,6 +763,11 @@ namespace AgroBase.Forms.Operacoes private void chbPulverizador_CheckedChanged(object sender, EventArgs e) { Variaveis.OperacaoEmAndamento.Parametros.Controle.PulverizadorAutomatico = ((CheckBox)sender).Checked; + + if (!Variaveis.OperacaoEmAndamento.Parametros.Controle.PulverizadorAutomatico && Variaveis.OperacaoEmAndamento.Controle.Bicos.Any(x => x.ComandoAtuar)) + { + Variaveis.OperacaoEmAndamento.Controle.Bicos.ForEach(b => b.ComandoAtuar = false); + } } private void chbSonarAtivado_CheckedChanged(object sender, EventArgs e) diff --git a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs index 4fe05167c..e58efb3de 100644 --- a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs +++ b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs @@ -593,6 +593,7 @@ namespace AgroBase.Models.Modules Sensor_ID_Num = 11, ComandoAtuar = false, PressaoSP = 100, + PotenciaSP = 100, Componente = S_Code.sBMB, Comandar = true, Mandatorio = true, diff --git a/AgroBase/AgroBase/Models/TrajetoriaMapaOperacaoModel.cs b/AgroBase/AgroBase/Models/TrajetoriaMapaOperacaoModel.cs index 56e136560..691331cee 100644 --- a/AgroBase/AgroBase/Models/TrajetoriaMapaOperacaoModel.cs +++ b/AgroBase/AgroBase/Models/TrajetoriaMapaOperacaoModel.cs @@ -200,6 +200,12 @@ namespace AgroBase.Models { CorredorAtual?.AtualizarDados(); + //AtualizarDadosAutonomiaCorredor(idxCorredor: idxNovoCorredor); + } + + bool mudouCorredor = (CorredorAtual?.Idx ?? -1) != idxNovoCorredor; + if (mudouCorredor || !AutonomiaCorredor.Liberado) + { AtualizarDadosAutonomiaCorredor(idxCorredor: idxNovoCorredor); } diff --git a/AgroBase/AgroBase/Services/GPSService.cs b/AgroBase/AgroBase/Services/GPSService.cs index e2d5e19b9..33b345216 100644 --- a/AgroBase/AgroBase/Services/GPSService.cs +++ b/AgroBase/AgroBase/Services/GPSService.cs @@ -98,7 +98,7 @@ namespace AgroBase.Services public static async Task ConfigurarModulo() { Variaveis.MostrarLog($"[GPSService.ConfigurarModulo] Iniciando configuração do módulo GPS..."); - await ConfigurarModuloRover(comprimento_antena: 130); + await ConfigurarModuloRover(comprimento_antena: 100); } private static async Task ConfigurarModuloRover(string porta_usb = "com3", string porta_entrada = "com2", int comprimento_antena = 100, int tolerancia_antena = 5) diff --git a/AgroBase/AgroBase/Services/HerbicideCounterService.cs b/AgroBase/AgroBase/Services/HerbicideCounterService.cs index 08a72b17c..f7ea5f2be 100644 --- a/AgroBase/AgroBase/Services/HerbicideCounterService.cs +++ b/AgroBase/AgroBase/Services/HerbicideCounterService.cs @@ -377,18 +377,40 @@ namespace AgroBase.Services private double CalcularLPorMetroReferencia(double velocidadeAtual_m_s) { + // 1. Histórico real já consolidado if (LPorMetroHistorico > 0) return LPorMetroHistorico; + // 2. Janela atual real if (LPorMetro > 0) return LPorMetro; - double velocidade_m_min = velocidadeAtual_m_s * 60.0; + // 3. Pré-cálculo enquanto o robô ainda está parado + double velocidadeReferencia_m_s = velocidadeAtual_m_s; - if (velocidade_m_min > 0.1 && LPorMinutoEstimadoBicos > 0) - return LPorMinutoEstimadoBicos / velocidade_m_min; + if (velocidadeReferencia_m_s < 0.10) + { + velocidadeReferencia_m_s = + FuncoesMatematicas.CalculaVelocidadeMsPercentual( + Variaveis.OperacaoEmAndamento + .Parametros + .Controle + .MovVelocidadeCErvasPercent + ); + } - return LPorMetroBase; + // Para a verificação pré-corredor, usar o pior caso: + // todos os bicos pulverizando. + double vazaoReferencia_LMin = VazaoTotalTodosBicos_LMin; + + if (velocidadeReferencia_m_s > 0.01 && + vazaoReferencia_LMin > 0) + { + double velocidade_m_min = velocidadeReferencia_m_s * 60.0; + return vazaoReferencia_LMin / velocidade_m_min; + } + + return 0; } public bool ConseguePulverizarRua(double comprimentoRua_m) diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/regras_taticas.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/regras_taticas.py index fa5acc45d..aebee9e90 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/regras_taticas.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/regras_taticas.py @@ -65,75 +65,187 @@ class RegrasTaticas: _imu = ContextoGlobalRedis.get_modulo(T_Code.Imu) or {} _imu_saude = StatusModulo( - _imu.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value) + _imu.get("saude", {}).get( + "status", + StatusModulo.DESCONECTADO.value + ) ) - imu_operante = _imu_saude in [StatusModulo.OPERANTE, StatusModulo.ALERTA] + imu_operante = _imu_saude in [ + StatusModulo.OPERANTE, + StatusModulo.ALERTA, + ] - imu_ts = float(_imu.get("timestamp", 0.0) or 0.0) - imu_ts_s = imu_ts / 1000.0 if imu_ts > 100000000000.0 else imu_ts - imu_atualizada = (time.time() - imu_ts_s) <= 1.0 if imu_ts_s > 0 else False + # ============================================================ + # Atualidade do dado + # ============================================================ + + try: + imu_ts = float(_imu.get("timestamp", 0.0) or 0.0) + except (TypeError, ValueError): + imu_ts = 0.0 + + imu_ts_s = ( + imu_ts / 1000.0 + if imu_ts > 100000000000.0 + else imu_ts + ) + + imu_atualizada = ( + (time.time() - imu_ts_s) <= 1.0 + if imu_ts_s > 0 + else False + ) imu_valido = bool(_imu.get("valido", False)) - risk_level = str(_imu.get("risk_level", "unknown") or "unknown") - risk_level_num = int(_imu.get("risk_level_num", 4) or 4) + # ============================================================ + # Estado de risco + # ============================================================ + + risk_level = str( + _imu.get("risk_level", "unknown") or "unknown" + ).strip().lower() + + risk_level_num_raw = _imu.get("risk_level_num", None) + + if risk_level_num_raw is None: + # Fallback pelo campo textual caso o número não tenha sido publicado. + mapa_niveis = { + "normal": 0, + "attention": 1, + "risk": 2, + "critical": 3, + "emergency": 4, + } + + risk_level_num = mapa_niveis.get(risk_level, 4) + else: + try: + risk_level_num = int(risk_level_num_raw) + except (TypeError, ValueError): + risk_level_num = 4 + + # Impede valores fora do contrato. + risk_level_num = max(0, min(4, risk_level_num)) stop = bool(_imu.get("stop", False)) emergency_stop = bool(_imu.get("emergency_stop", False)) bloquear_avanco = bool(_imu.get("bloquear_avanco", False)) - roll = float(_imu.get("roll_filtrado", _imu.get("roll_seg", _imu.get("roll", 0.0))) or 0.0) - pitch = float(_imu.get("pitch_filtrado", _imu.get("pitch_seg", _imu.get("pitch", 0.0))) or 0.0) + try: + roll = float( + _imu.get( + "roll_filtrado", + _imu.get("roll_seg", _imu.get("roll", 0.0)) + ) or 0.0 + ) + except (TypeError, ValueError): + roll = 0.0 - roll_rate = float(_imu.get("roll_rate_dps", 0.0) or 0.0) - pitch_rate = float(_imu.get("pitch_rate_dps", 0.0) or 0.0) + try: + pitch = float( + _imu.get( + "pitch_filtrado", + _imu.get("pitch_seg", _imu.get("pitch", 0.0)) + ) or 0.0 + ) + except (TypeError, ValueError): + pitch = 0.0 + + try: + roll_rate = float( + _imu.get("roll_rate_dps", 0.0) or 0.0 + ) + except (TypeError, ValueError): + roll_rate = 0.0 + + try: + pitch_rate = float( + _imu.get("pitch_rate_dps", 0.0) or 0.0 + ) + except (TypeError, ValueError): + pitch_rate = 0.0 motivos_risco = _imu.get("motivos_risco", []) or [] - acao_sugerida = str(_imu.get("acao_sugerida", "") or "") - - imu_bloqueio_bruto = ( - not imu_operante - or not imu_atualizada - or not imu_valido - or emergency_stop - or stop - or bloquear_avanco - or risk_level_num >= 3 + acao_sugerida = str( + _imu.get("acao_sugerida", "") or "" ) - inclinacao_persistente = self._avaliar_persistencia( - nome="inclinacao_perigosa", - estado_bruto=imu_bloqueio_bruto, - tempo_acionar=0.10 if emergency_stop else 0.20, - tempo_liberar=1.2 - ) + # ============================================================ + # Primeiro valida a disponibilidade da IMU + # ============================================================ if not imu_operante: + self._resetar_persistencia("inclinacao_perigosa") + motivos.append( - f"IMU mandatória por [ Parada por Inclinação ] não operante: {_imu_saude.name}" + "IMU mandatória por [ Parada por Inclinação ] " + f"não operante: {_imu_saude.name}" ) + freio_necessario = True elif not imu_atualizada: - motivos.append("IMU mandatória por [ Parada por Inclinação ] desatualizada") + self._resetar_persistencia("inclinacao_perigosa") + + motivos.append( + "IMU mandatória por [ Parada por Inclinação ] " + "desatualizada" + ) + freio_necessario = True elif not imu_valido: - motivos.append("IMU mandatória por [ Parada por Inclinação ] sem leitura válida") - freio_necessario = True + self._resetar_persistencia("inclinacao_perigosa") - elif inclinacao_persistente: motivos.append( - f"IMU bloqueou movimento: {risk_level} | ação={acao_sugerida} | " - f"roll={roll:.2f}°, pitch={pitch:.2f}°, " - f"roll_rate={roll_rate:.1f}°/s, pitch_rate={pitch_rate:.1f}°/s" + "IMU mandatória por [ Parada por Inclinação ] " + "sem leitura válida" ) - if motivos_risco: - motivos.append(f"Motivos IMU: {', '.join(map(str, motivos_risco))}") + freio_necessario = True - freio_necessario = bool(emergency_stop or stop or risk_level_num >= 3) + else: + # A persistência passa a tratar somente perigo de inclinação, + # e não falhas de comunicação ou saúde. + perigo_inclinacao_bruto = bool( + emergency_stop + or stop + or bloquear_avanco + or risk_level_num >= 3 + ) + + inclinacao_persistente = self._avaliar_persistencia( + nome="inclinacao_perigosa", + estado_bruto=perigo_inclinacao_bruto, + tempo_acionar=0.10 if emergency_stop else 0.20, + tempo_liberar=1.2, + ) + + if inclinacao_persistente: + motivos.append( + f"IMU bloqueou movimento: {risk_level} | " + f"nível={risk_level_num} | " + f"ação={acao_sugerida} | " + f"roll={roll:.2f}°, pitch={pitch:.2f}°, " + f"roll_rate={roll_rate:.1f}°/s, " + f"pitch_rate={pitch_rate:.1f}°/s" + ) + + if motivos_risco: + motivos.append( + f"Motivos IMU: " + f"{', '.join(map(str, motivos_risco))}" + ) + + # Se a persistência está ativa, o bloqueio também deve estar. + # Não devemos recalcular usando somente o estado instantâneo. + freio_necessario = True + else: + # Evita que um bloqueio antigo sobreviva ao recurso ser desligado + # e ligado novamente. + self._resetar_persistencia("inclinacao_perigosa") pulverizador_automatico = _controle.get("pulverizador_automatico", False) if pulverizador_automatico: @@ -159,7 +271,6 @@ class RegrasTaticas: f"Cavitação na bomba pressurizadora da linha! " f"Pressão: {pressao_sp}/{pressao_atual} psi" ) - freio_necessario = False mov_assistido_sonar = _controle.get("mov_auxilio_sonar", False) dir_assistido_sonar = _controle.get("dir_auxilio_sonar", False) @@ -205,3 +316,14 @@ class RegrasTaticas: p["ultimo_estado_bruto"] = estado_bruto return p["ativo"] + + def _resetar_persistencia(self, nome): + p = self.persistencias.get(nome) + + if p is None: + return + + p["ativo"] = False + p["ts_inicio_true"] = None + p["ts_inicio_false"] = None + p["ultimo_estado_bruto"] = False diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.py index d97ceb405..70ea9dbef 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/config.py @@ -73,7 +73,7 @@ WEED_DEFAULT_CONFIG = { "camera_height": 800, # FPS solicitado na câmera/OAK. Pode ser maior que o pipeline. - "camera_fps": 25, + "camera_fps": 40, # Tamanho final do tensor entregue ao modelo: [W, H]. "ia_resolution": [640, 400], @@ -126,6 +126,7 @@ WEED_DEFAULT_CONFIG = { # ============================================================ # Gate global: só libera bicos quando existe alvo suficiente no frame. "usar_radar_global_gate": True, + "inverter_ordem_bicos": True, # Histerese global da fração de alvo no frame completo. "min_frac_erva_global_on": 0.0020, diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/weed_detector.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/weed_detector.py index baecbf6c5..64d6f9962 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/weed_detector.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/weed_detector.py @@ -58,6 +58,8 @@ class WeedDetector: if self.qtd_bicos <= 0: self.qtd_bicos = 7 + self.inverter_ordem_bicos = bool(self.config.get("inverter_ordem_bicos", False)) + self.detector_debug_perf = bool( self.config.get("detector_debug_perf", False) ) @@ -125,12 +127,25 @@ class WeedDetector: novo_qtd_bicos = int( self.config.get("qtd_bicos", self.qtd_bicos) or self.qtd_bicos ) + novo_inverter_ordem_bicos = bool( + self.config.get( + "inverter_ordem_bicos", + self.inverter_ordem_bicos, + ) + ) if novo_qtd_bicos <= 0: novo_qtd_bicos = 7 - if novo_qtd_bicos != self.qtd_bicos: - self.qtd_bicos = novo_qtd_bicos + mudou_mapeamento = ( + novo_qtd_bicos != self.qtd_bicos + or novo_inverter_ordem_bicos != self.inverter_ordem_bicos + ) + + self.qtd_bicos = novo_qtd_bicos + self.inverter_ordem_bicos = novo_inverter_ordem_bicos + + if mudou_mapeamento: self._inicializar_estado() self.detector_debug_perf = bool( @@ -343,6 +358,14 @@ class WeedDetector: larguras = np.diff(edges).astype(np.int32) area_por_bico = alturas * larguras + # A segmentação é calculada naturalmente da esquerda para a direita. + # Quando habilitado, inverte a associação entre região da imagem + # e numeração física dos bicos. + if self.inverter_ordem_bicos: + px_target_por_bico = px_target_por_bico[::-1].copy() + larguras = larguras[::-1].copy() + area_por_bico = area_por_bico[::-1].copy() + with np.errstate(divide="ignore", invalid="ignore"): frac_target_por_bico = np.where( area_por_bico > 0, @@ -361,6 +384,7 @@ class WeedDetector: # Semântica atual: fração de target/alvo pulverizável. "frac_erva_por_bico": frac_target_por_bico, "ema_frac_por_bico": self._ema_frac_bico.copy(), + "ordem_bicos_invertida": bool(self.inverter_ordem_bicos), "faixa": { "y_top": int(y_top), "y_bot": int(y_bot),