diff --git a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs index 30f62de1c..e4f2add2d 100644 --- a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs +++ b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs @@ -232,7 +232,8 @@ namespace AgroBase.Models.Modules maximo = 80, minimo = 5, nominal = 25, - unidade_medida = "ºC" + unidade_medida = "ºC", + offset = -39.9, }, new SensorValoresLeituraModel() { @@ -288,7 +289,8 @@ namespace AgroBase.Models.Modules maximo = 80, minimo = 5, nominal = 25, - unidade_medida = "ºC" + unidade_medida = "ºC", + offset = -0.5, }, new SensorValoresLeituraModel() { @@ -344,7 +346,8 @@ namespace AgroBase.Models.Modules maximo = 80, minimo = 5, nominal = 25, - unidade_medida = "ºC" + unidade_medida = "ºC", + offset = -15.9, }, new SensorValoresLeituraModel() { @@ -400,7 +403,8 @@ namespace AgroBase.Models.Modules maximo = 80, minimo = 5, nominal = 25, - unidade_medida = "ºC" + unidade_medida = "ºC", + offset = -3.6, }, new SensorValoresLeituraModel() { diff --git a/AgroBase/AgroBase/Models/TrajetoriaMapaOperacaoModel.cs b/AgroBase/AgroBase/Models/TrajetoriaMapaOperacaoModel.cs index 5baace0e1..a0306496c 100644 --- a/AgroBase/AgroBase/Models/TrajetoriaMapaOperacaoModel.cs +++ b/AgroBase/AgroBase/Models/TrajetoriaMapaOperacaoModel.cs @@ -47,7 +47,7 @@ namespace AgroBase.Models public AutonomiaCorredorModel AutonomiaCorredor { get; set; } [JsonIgnore] - private GPSModel GPSPosicaoAtual => Variaveis.OperacaoEmAndamento.Sensoriamento?.Gps?.Clone(); + private GPSModel GPSPosicaoAtual => GPSService.UltimaLeitura?.Clone(); [JsonProperty] public double TempoEntreLeituras { get; private set; } @@ -2155,8 +2155,6 @@ namespace AgroBase.Models int idxEsq = 0; int idxDir = 0; - double anguloAcrescentar = direcaoAtual == DirecaoCarroRua.Ida ? AnguloAberturaCurva : -AnguloAberturaCurva; - for (int idx = 0; idx < Corredores.Count(); idx++) { double _FatorLarguraCorredor = CorredoresLarguras[idx] / LarguraCorredorPadrao; @@ -2282,15 +2280,29 @@ namespace AgroBase.Models _trajetoriaFixa.Add(PontoTrajetoria); } - double anguloProjetar2 = GPSUtils.CalcularOrientacao(CorredorAtual.Skip(CorredorAtual.Count() - 2).First(), CorredorAtual.Last()); + double percentualAcrescimoPontoAberturaCurva = 0.8; + double distancia_projetar = !ultimoCorredor ? (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva) : (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva); + + GPSModel ultimoPontoCorredorAtual = CorredorAtual.Last(); + double anguloProjetar2 = GPSUtils.CalcularOrientacao(CorredorAtual.Skip(CorredorAtual.Count() - 2).First(), ultimoPontoCorredorAtual); + GPSModel UltimoPonto = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2); + // Realiza um acréscimo no angulo à projetar, permitindo que o equipamento faça uma curva mais aberta entre corredores if (!ultimoCorredor && !_FatorLarguraElevado) { - anguloProjetar2 += anguloAcrescentar; - anguloAcrescentar *= -1; + GPSModel up_pos = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2 + AnguloAberturaCurva); + GPSModel up_neg = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2 - AnguloAberturaCurva); + + GPSModel pp_proximoCorredor = direcaoAtual == DirecaoCarroRua.Volta ? Corredores[idx + 1].FirstOrDefault() : Corredores[idx + 1].LastOrDefault(); + + double d_pos = GPSUtils.DistanciaEntrePontos(pp_proximoCorredor, up_pos); + double d_neg = GPSUtils.DistanciaEntrePontos(pp_proximoCorredor, up_neg); + + if (d_pos > d_neg) + UltimoPonto = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2 + AnguloAberturaCurva); + else + UltimoPonto = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2 - AnguloAberturaCurva); } - double percentualAcrescimoPontoAberturaCurva = 0.8; - GPSModel UltimoPonto = GPSUtils.ProjetarPontoDeslocado(CorredorAtual.Last(), !ultimoCorredor ? (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva) : (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva), anguloProjetar2); PontoTrajetoriaModel PontoFinal = new PontoTrajetoriaModel(TipoPontoRua.LigacaoSaida) { diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_imu.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_imu.py index 92371b48b..d38b141dd 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_imu.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_imu.py @@ -86,8 +86,13 @@ class IMUCamera(ModuloDiagnosticoBase): self.calibrado = False self.calibrando = False self.calib_inicio_ts = 0.0 + self.calib_primeira_amostra_quat_ts = None + self.calib_min_amostras_quat = max(30, int(self.freq * 0.50)) + self.calib_max_std_deg = 1.50 self.calib_quats_xyzw = [] self.q_ref_inv = None + self.rotation_accuracy_rad = None + self.rotation_accuracy_max_rad = math.radians(20.0) # Estado angular self.roll_deg = 0.0 @@ -176,7 +181,9 @@ class IMUCamera(ModuloDiagnosticoBase): self.calibrado = False self.calibrando = True - self.calib_inicio_ts = time.perf_counter() + # A janela útil começa somente quando chegar a primeira amostra válida. + self.calib_inicio_ts = 0.0 + self.calib_primeira_amostra_quat_ts = None self.calib_quats_xyzw.clear() self._calib_roll_hist.clear() @@ -580,7 +587,12 @@ class IMUCamera(ModuloDiagnosticoBase): if not self.calibrando: return - self.calib_quats_xyzw.append(q_xyzw) + agora = time.perf_counter() + if self.calib_primeira_amostra_quat_ts is None: + self.calib_primeira_amostra_quat_ts = agora + self.calib_inicio_ts = agora + + self.calib_quats_xyzw.append(np.asarray(q_xyzw, dtype=np.float64)) try: roll_abs, pitch_abs, _ = R.from_quat(q_xyzw).as_euler("xyz", degrees=True) @@ -589,9 +601,28 @@ class IMUCamera(ModuloDiagnosticoBase): except Exception: pass - elapsed = time.perf_counter() - self.calib_inicio_ts + elapsed = agora - self.calib_primeira_amostra_quat_ts - if elapsed < self.calibracao_segundos: + if ( + elapsed < self.calibracao_segundos + or len(self.calib_quats_xyzw) < self.calib_min_amostras_quat + ): + return + + roll_std = float(np.std(self._calib_roll_hist)) if self._calib_roll_hist else 999.0 + pitch_std = float(np.std(self._calib_pitch_hist)) if self._calib_pitch_hist else 999.0 + + # Não aceita como zero uma janela em que o rover/câmera se moveu. + if roll_std > self.calib_max_std_deg or pitch_std > self.calib_max_std_deg: + self.mostrar_log( + f"Calibração rejeitada por movimento | std_roll={roll_std:.3f}° | " + f"std_pitch={pitch_std:.3f}°. Reiniciando janela." + ) + self.calib_quats_xyzw.clear() + self._calib_roll_hist.clear() + self._calib_pitch_hist.clear() + self.calib_primeira_amostra_quat_ts = None + self.calib_inicio_ts = 0.0 return q_ref = self._media_quaternion_xyzw(self.calib_quats_xyzw) @@ -607,9 +638,6 @@ class IMUCamera(ModuloDiagnosticoBase): self.calibrando = False self.calibrado = True - roll_std = float(np.std(self._calib_roll_hist)) if self._calib_roll_hist else 0.0 - pitch_std = float(np.std(self._calib_pitch_hist)) if self._calib_pitch_hist else 0.0 - self.mostrar_log( f"Calibração concluída | amostras={len(self.calib_quats_xyzw)} | " f"std_roll={roll_std:.3f}° | std_pitch={pitch_std:.3f}°" @@ -762,7 +790,6 @@ class IMUCamera(ModuloDiagnosticoBase): if q_xyzw is None: return False - agora = time.perf_counter() agora_wall = time.time() self.last_packet_ts = agora_wall @@ -770,6 +797,8 @@ class IMUCamera(ModuloDiagnosticoBase): self._processar_calibracao(q_xyzw) + self.rotation_accuracy_rad = accuracy_rad + roll, pitch, yaw = self._quat_relativo_para_euler(q_xyzw) self.roll_deg = roll @@ -825,6 +854,11 @@ class IMUCamera(ModuloDiagnosticoBase): # Metadados do rotation vector. "rotation_accuracy_rad": accuracy_rad, + "rotation_accuracy_ok": bool( + accuracy_rad is not None + and math.isfinite(float(accuracy_rad)) + and 0.0 <= float(accuracy_rad) <= self.rotation_accuracy_max_rad + ), # Saúde operacional básica. "timestamp": agora_wall * 1000.0, diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_multispectral.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_multispectral.py index 776a15162..e02b74f3e 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_multispectral.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_multispectral.py @@ -808,18 +808,26 @@ class CameraMultispectral: ) performance["device_metrics"] = metrics - temp = float(metrics.get("temperature_average_c", 0.0) or 0.0) - performance["temperatura"] = temp - - if temp >= 80: - motivos.append(f"Temperatura crítica: {temp:.1f} °C") - saude -= 30 - elif temp >= 70: - motivos.append(f"Temperatura elevada: {temp:.1f} °C") - saude -= 15 - elif temp >= 60: - motivos.append(f"Temperatura acima do ideal: {temp:.1f} °C") - saude -= 5 + temp_raw = metrics.get("temperature_average_c") + if temp_raw is None: + performance["temperatura"] = None + motivos.append("Temperatura interna indisponível") + saude -= 10 + else: + temp = float(temp_raw) + performance["temperatura"] = temp + if temp >= 100: + motivos.append(f"Temperatura de emergência: {temp:.1f} °C") + saude = min(saude, 10) + elif temp >= 95: + motivos.append(f"Temperatura crítica: {temp:.1f} °C") + saude = min(saude, 35) + elif temp >= 85: + motivos.append(f"Temperatura elevada: {temp:.1f} °C") + saude -= 15 + elif temp >= 75: + motivos.append(f"Aquecimento preventivo: {temp:.1f} °C") + saude -= 5 ddr = float(metrics.get("ddr_used_bytes", 0) or 0) / 1024.0 / 1024.0 performance["memoria_usada"] = ddr diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_oak.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_oak.py index 58be478c4..085ced3c9 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_oak.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/camera_worker/camera_oak.py @@ -138,7 +138,7 @@ class CameraOak: nome_sensor=f"{self.modelo}_imu", modo=self.imu_modo, gyro_unidade="rad_s", - referencial_robo="r,p,y", + referencial_robo="y,r,p", ) if self.modelo_ia_seg is not None: @@ -634,15 +634,24 @@ class CameraOak: "velocidade": speed } # 🔸 Temperatura (> 60 °C começa a penalizar) - if temp >= 80: - motivos.append(f"Temperatura crítica: {temp:.1f} °C") - saude -= 30 - elif temp >= 70: - motivos.append(f"Temperatura elevada: {temp:.1f} °C") - saude -= 15 - elif temp >= 60: - motivos.append(f"Temperatura acima do ideal: {temp:.1f} °C") - saude -= 5 + if temp is None: + performance["temperatura"] = None + motivos.append("Temperatura interna indisponível") + saude -= 10 + else: + temp = float(temp) + if temp >= 100: + motivos.append(f"Temperatura de emergência: {temp:.1f} °C") + saude = min(saude, 10) + elif temp >= 95: + motivos.append(f"Temperatura crítica: {temp:.1f} °C") + saude = min(saude, 35) + elif temp >= 85: + motivos.append(f"Temperatura elevada: {temp:.1f} °C") + saude -= 15 + elif temp >= 75: + motivos.append(f"Aquecimento preventivo: {temp:.1f} °C") + saude -= 5 # 🔸 Memória DDR (> 300 MB começa a penalizar) if ddr >= 500: diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py index 9a1a1f0c9..f96b2bff1 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py @@ -35,6 +35,7 @@ class CameraIMU(ModuloDiagnosticoBase): timeout_imu_s=0.40, timeout_camera_s=2.0, max_disagreement_deg=4.0, + rotation_accuracy_max_rad=0.35, # Limites principais vindos do equipamento. # No rover: @@ -68,6 +69,7 @@ class CameraIMU(ModuloDiagnosticoBase): self.timeout_imu_s = float(timeout_imu_s) self.timeout_camera_s = float(timeout_camera_s) self.max_disagreement_deg = float(max_disagreement_deg) + self.rotation_accuracy_max_rad = float(rotation_accuracy_max_rad) # ============================================================ # Limites angulares do equipamento @@ -429,14 +431,13 @@ class CameraIMU(ModuloDiagnosticoBase): # timestamp pode vir em ms. ts_s = ts_ms / 1000.0 if ts_ms > 100000000000.0 else ts_ms - candidatos_ts = [ - ts_s if ts_s > 0 else 0.0, - last_packet_ts if last_packet_ts > 0 else 0.0, - last_publish_ts if last_publish_ts > 0 else 0.0, - ] - - ts_ref = max(candidatos_ts) - idade_s = (agora - ts_ref) if ts_ref > 0 else 999.0 + # Todos os campos acima devem estar no relógio de parede (Unix epoch). + # Não usa max() cegamente: um timestamp monotônico misturado ao epoch + # pode esconder uma fonte velha ou produzir idade negativa. + ts_ref = ts_s if ts_s > 0 else last_packet_ts + if ts_ref <= 0: + ts_ref = last_publish_ts + idade_s = max(0.0, agora - ts_ref) if ts_ref > 0 else 999.0 roll = self._safe_float(imu, "roll", 0.0) pitch = self._safe_float(imu, "pitch", 0.0) @@ -450,6 +451,9 @@ class CameraIMU(ModuloDiagnosticoBase): pitch_rate = self._safe_float(imu, "pitch_rate_dps", 0.0) yaw_rate = self._safe_float(imu, "yaw_rate_dps", 0.0) + modo = str(imu.get("modo", "") or "").lower() + accuracy_rad = self._safe_float(imu, "rotation_accuracy_rad", float("nan")) + imu_saude = None try: saude_obj = imu.get("saude") @@ -491,6 +495,29 @@ class CameraIMU(ModuloDiagnosticoBase): confiavel = False motivos.append(f"peso_base_baixo") + valores = (roll_filtrado, pitch_filtrado, yaw_filtrado, + roll_rate, pitch_rate, yaw_rate) + if not all(math.isfinite(v) for v in valores): + confiavel = False + motivos.append("imu_valor_nao_finito") + + accuracy_score = 1.0 + if modo in ("rotation_vector", "game_rotation_vector"): + if not math.isfinite(accuracy_rad) or accuracy_rad < 0.0: + confiavel = False + accuracy_score = 0.0 + motivos.append("rotation_accuracy_indisponivel") + elif accuracy_rad > self.rotation_accuracy_max_rad: + confiavel = False + accuracy_score = 0.0 + motivos.append(f"rotation_accuracy_ruim_{accuracy_rad:.3f}rad") + else: + accuracy_score = self.clamp( + 1.0 - accuracy_rad / self.rotation_accuracy_max_rad, + 0.25, + 1.0, + ) + freshness = 1.0 - self.norm_to_100(idade_s, 0.0, self.timeout_imu_s) / 100.0 freshness = self.clamp(freshness, 0.0, 1.0) @@ -501,7 +528,10 @@ class CameraIMU(ModuloDiagnosticoBase): if imu_saude is not None: saude_score = self.clamp(imu_saude / 100.0, 0.0, 1.0) - confidence = peso_base * freshness * calib_score * valido_score * saude_score + confidence = ( + peso_base * freshness * calib_score * valido_score + * saude_score * accuracy_score + ) if not confiavel: confidence = 0.0 @@ -525,9 +555,11 @@ class CameraIMU(ModuloDiagnosticoBase): "confidence": float(confidence), "peso_base": float(peso_base), - "roll": float(roll), - "pitch": float(pitch), - "yaw": float(yaw), + # O módulo final consome uma atitude coerente: os três ângulos + # filtrados pertencem à mesma fonte e ao mesmo instante publicado. + "roll": float(roll_filtrado), + "pitch": float(pitch_filtrado), + "yaw": float(yaw_filtrado), "roll_filtrado": float(roll_filtrado), "pitch_filtrado": float(pitch_filtrado), @@ -536,6 +568,7 @@ class CameraIMU(ModuloDiagnosticoBase): "roll_rate_dps": float(roll_rate), "pitch_rate_dps": float(pitch_rate), "yaw_rate_dps": float(yaw_rate), + "rotation_accuracy_rad": accuracy_rad if math.isfinite(accuracy_rad) else None, "timestamp": ts_ms, "last_packet_ts": last_packet_ts, @@ -790,11 +823,18 @@ class CameraIMU(ModuloDiagnosticoBase): "divergentes": [], } + def maior_distancia_circular(valores): + maior = 0.0 + for i, a in enumerate(valores): + for b in valores[i + 1:]: + maior = max(maior, abs(self._angle_delta_deg(a, b))) + return float(maior) + rolls = [float(f["roll"]) for f in fontes] pitchs = [float(f["pitch"]) for f in fontes] - roll_dis = float(max(rolls) - min(rolls)) - pitch_dis = float(max(pitchs) - min(pitchs)) + roll_dis = maior_distancia_circular(rolls) + pitch_dis = maior_distancia_circular(pitchs) max_dis = max(roll_dis, pitch_dis) sensores_agree = max_dis <= self.max_disagreement_deg @@ -817,17 +857,6 @@ class CameraIMU(ModuloDiagnosticoBase): "divergentes": divergentes, } - def _valor_mais_conservador(self, fontes, key): - fonte = max( - fontes, - key=lambda f: abs(float(f.get(key, 0.0))), - ) - - return ( - float(fonte.get(key, 0.0)), - fonte["mx_id"], - ) - def _fundir_fontes(self, fontes, sensores_agree=True): if not fontes: return None @@ -879,41 +908,26 @@ class CameraIMU(ModuloDiagnosticoBase): "estrategia_fusao": "weighted_agreement", } - roll, fonte_roll = self._valor_mais_conservador( - fontes, - "roll", - ) - pitch, fonte_pitch = self._valor_mais_conservador( - fontes, - "pitch", - ) - roll_rate, fonte_roll_rate = self._valor_mais_conservador( - fontes, - "roll_rate_dps", - ) - pitch_rate, fonte_pitch_rate = self._valor_mais_conservador( - fontes, - "pitch_rate_dps", - ) - + # Em divergência, não cria uma orientação artificial misturando eixos. + # Isola as fontes secundárias e usa o pacote completo da mais confiável. return { - "roll": roll, - "pitch": pitch, + "roll": fonte_principal["roll"], + "pitch": fonte_principal["pitch"], "yaw": fonte_principal["yaw"], - "roll_rate_dps": roll_rate, - "pitch_rate_dps": pitch_rate, + "roll_rate_dps": fonte_principal["roll_rate_dps"], + "pitch_rate_dps": fonte_principal["pitch_rate_dps"], "yaw_rate_dps": fonte_principal["yaw_rate_dps"], "confidence": min( float(fonte_principal["confidence"]), 1.0, ), "fonte_principal": fonte_principal["mx_id"], - "fonte_roll": fonte_roll, - "fonte_pitch": fonte_pitch, - "fonte_roll_rate": fonte_roll_rate, - "fonte_pitch_rate": fonte_pitch_rate, + "fonte_roll": fonte_principal["mx_id"], + "fonte_pitch": fonte_principal["mx_id"], + "fonte_roll_rate": fonte_principal["mx_id"], + "fonte_pitch_rate": fonte_principal["mx_id"], "single_source": False, - "estrategia_fusao": "conservative_disagreement", + "estrategia_fusao": "primary_source_on_disagreement", } def _atualizar_filtro_fusao(self, fusao): @@ -1747,4 +1761,4 @@ class CameraIMU(ModuloDiagnosticoBase): return dict(self.last_data) def mostrar_log(self, mensagem): - print(f"{time.time()} - [CameraIMUFinal][id={id(self)}] {mensagem}") \ No newline at end of file + print(f"{time.time()} - [CameraIMUFinal][id={id(self)}] {mensagem}") diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/movimentacao.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/movimentacao.py index 7bf1d5c98..dab0d4b21 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/movimentacao.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/movimentacao.py @@ -47,6 +47,33 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): PENALIDADE_FREQ_MAX_PADRAO = 4 PENALIDADE_FREQ_MAX_CRITICA = 8 + # Temperatura MOV + TEMP_ALERTA_C = 70.0 + TEMP_FALHA_C = 85.0 + TEMP_EMERGENCIA_C = 100.0 + + TEMP_RECUPERA_ALERTA_C = 67.0 + TEMP_RECUPERA_FALHA_C = 80.0 + TEMP_RECUPERA_EMERGENCIA_C = 90.0 + + TEMP_ALERTA_INTERMEDIARIO_S = 3.0 + TEMP_ALERTA_CONFIRMADO_S = 10.0 + + TEMP_FALHA_INTERMEDIARIO_S = 2.0 + TEMP_FALHA_CONFIRMADO_S = 5.0 + + TEMP_EMERGENCIA_CONFIRMADO_S = 1.0 + TEMP_RECUPERACAO_S = 10.0 + + # Elétricos + TENSAO_FALHA_PERSISTENCIA_S = 2.0 + CORRENTE_MOTOR_FALHA_PERSISTENCIA_S = 1.0 + CORRENTE_BARRAMENTO_FALHA_PERSISTENCIA_S = 1.0 + + # Freio/comando + FREIO_RPM_FALHA_PERSISTENCIA_S = 1.0 + ALARME_COMANDO_PERSISTENCIA_S = 2.0 + def __init__(self): self.t_code = T_Code.Mov self.nome = "Movimentacao" @@ -89,6 +116,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): 19: ("Sinal Z não encontrado", 25, False), } + self._estados_persistencia = {} + def atualizar_saude(self): try: dados_mov = ContextoGlobalRedis.get_modulo(self.t_code) or {} @@ -543,7 +572,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): # Faixas absolutas. Ajustar depois com logs reais. if tensao > 0: - if tensao < 24 or tensao > 50: + if tensao < 25 or tensao > 45: motivos.append(f"Tensão MOV em faixa perigosa: {tensao:.1f} V") condicoes.append({ "label": "Tensão MOV", @@ -554,7 +583,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): }) score -= 70 risco_eletrico_termico = True - elif tensao < 28 or tensao > 45: + elif tensao < 30 or tensao > 43: motivos.append(f"Tensão MOV fora da faixa esperada: {tensao:.1f} V") condicoes.append({ "label": "Tensão MOV", @@ -579,7 +608,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): }) score -= 75 risco_eletrico_termico = True - elif abs(corrente_motor) > 45: + elif abs(corrente_motor) > 50: motivos.append(f"Corrente do motor elevada: {corrente_motor:.1f} A") condicoes.append({ "label": "Corrente motor", @@ -615,37 +644,38 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): }) score -= 20 - temp_max = max(temperatura_motor, temperatura_driver) + # Temperatura do driver + _penalidade_d, _motivos_d, _condicoes_d, _risco_d = ( + self._avaliar_temperatura( + endereco=endereco, + temperatura=temperatura_driver, + regra_desc="driver", + ) + ) + motivos.extend(_motivos_d) + condicoes.extend(_condicoes_d) - if temp_max > 85: - motivos.append(f"Temperatura MOV perigosa: {temp_max:.1f} °C") - condicoes.append({ - "label": "Temperatura MOV", - "valor": temp_max, - "severidade": 95, - "descricao": f"Temperatura em faixa perigosa ({temp_max:.1f} °C)", - "acoes": [ - "Reduzir carga dos motores.", - "Verificar ventilação do compartimento.", - "Pausar operação se persistir.", - ], - }) - score -= 80 - risco_eletrico_termico = True - elif temp_max > 70: - motivos.append(f"Temperatura MOV elevada: {temp_max:.1f} °C") - condicoes.append({ - "label": "Temperatura MOV", - "valor": temp_max, - "severidade": 60, - "descricao": f"Temperatura elevada ({temp_max:.1f} °C)", - "acoes": [ - "Reduzir carga dos motores.", - "Verificar ventilação do compartimento dos drivers.", - ], - }) - score -= 25 + # Temperatura do motor + _penalidade_m, _motivos_m, _condicoes_m, _risco_m = ( + self._avaliar_temperatura( + endereco=endereco, + temperatura=temperatura_motor, + regra_desc="motor", + ) + ) + motivos.extend(_motivos_m) + condicoes.extend(_condicoes_m) + # Aplica somente a maior penalidade térmica. + score -= max(_penalidade_d, _penalidade_m) + + risco_eletrico_termico = ( + risco_eletrico_termico + or _risco_d + or _risco_m + ) + + # Padrões dinâmicos: úteis para diagnóstico, mas não travam operação. self._avaliar_padroes_dinamicos( padroes, @@ -752,6 +782,399 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): "parametros": parametros_debug, } + def _temperatura_valida(self, temperatura): + try: + valor = float(temperatura) + return 0.0 < valor < 200.0 + except (TypeError, ValueError): + return False + + def _avaliar_temperatura( + self, + endereco, + temperatura, + regra_desc, + ): + motivos = [] + condicoes = [] + penalidade = 0 + risco_eletrico_termico = False + + nome_origem = ( + "driver de movimento" + if regra_desc == "driver" + else "motor de tração" + ) + + label = f"Temperatura do {nome_origem}" + + if not self._temperatura_valida(temperatura): + motivos.append( + f"Temperatura do {nome_origem} inválida: " + f"{temperatura} °C" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 50, + "descricao": ( + "Leitura de temperatura inválida" + ), + "acoes": [ + "Verificar sensor de temperatura.", + ], + }) + return ( + penalidade, + motivos, + condicoes, + risco_eletrico_termico, + ) + + estado_temp_emergencia = self._estado_limite_alto( + driver=endereco, + regra=f"temperatura_emergencia_{regra_desc}", + valor=temperatura, + limite_entrada=self.TEMP_EMERGENCIA_C, + limite_saida=self.TEMP_RECUPERA_EMERGENCIA_C, + tempo_intermediario_s=0.0, + tempo_confirmacao_s=self.TEMP_EMERGENCIA_CONFIRMADO_S, + tempo_recuperacao_s=self.TEMP_RECUPERACAO_S, + ) + + estado_temp_falha = self._estado_limite_alto( + driver=endereco, + regra=f"temperatura_falha_{regra_desc}", + valor=temperatura, + limite_entrada=self.TEMP_FALHA_C, + limite_saida=self.TEMP_RECUPERA_FALHA_C, + tempo_intermediario_s=self.TEMP_FALHA_INTERMEDIARIO_S, + tempo_confirmacao_s=self.TEMP_FALHA_CONFIRMADO_S, + tempo_recuperacao_s=self.TEMP_RECUPERACAO_S, + ) + + estado_temp_alerta = self._estado_limite_alto( + driver=endereco, + regra=f"temperatura_alerta_{regra_desc}", + valor=temperatura, + limite_entrada=self.TEMP_ALERTA_C, + limite_saida=self.TEMP_RECUPERA_ALERTA_C, + tempo_intermediario_s=self.TEMP_ALERTA_INTERMEDIARIO_S, + tempo_confirmacao_s=self.TEMP_ALERTA_CONFIRMADO_S, + tempo_recuperacao_s=self.TEMP_RECUPERACAO_S, + ) + + # ============================================================ + # EMERGÊNCIA CONFIRMADA + # ============================================================ + + if estado_temp_emergencia["nivel"] == "confirmado": + motivos.append( + f"Temperatura do {nome_origem} em nível de emergência " + f"confirmado: {temperatura:.1f} °C" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 98, + "descricao": ( + "Temperatura de emergência confirmada após " + f"{estado_temp_emergencia['duracao_s']:.1f} s" + ), + "acoes": [ + "Interromper imediatamente o esforço dos motores.", + "Verificar ventilação, motor, driver e sensor térmico.", + ], + }) + + penalidade = 90 + risco_eletrico_termico = True + + # ============================================================ + # RECUPERANDO DE EMERGÊNCIA + # ============================================================ + + elif estado_temp_emergencia["nivel"] == "recuperando": + motivos.append( + f"Temperatura do {nome_origem} normalizando após emergência: " + f"{temperatura:.1f} °C. Aguardando estabilidade para liberação." + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 98, + "descricao": ( + "Temperatura caiu abaixo do limite de recuperação da " + "emergência, mas ainda aguarda estabilidade por " + f"{self.TEMP_RECUPERACAO_S:.1f} s " + f"({estado_temp_emergencia['duracao_s']:.1f} s decorridos)" + ), + "acoes": [ + "Manter o equipamento sem esforço.", + "Aguardar a conclusão da recuperação térmica.", + "Verificar se a temperatura continua diminuindo.", + ], + }) + + # Mantém exatamente a proteção da emergência enquanto recupera. + penalidade = 90 + risco_eletrico_termico = True + + # ============================================================ + # EMERGÊNCIA DETECTADA, AINDA NÃO CONFIRMADA + # ============================================================ + + elif estado_temp_emergencia["ativa"]: + motivos.append( + f"Temperatura de emergência do {nome_origem} detectada: " + f"{temperatura:.1f} °C " + f"({estado_temp_emergencia['duracao_s']:.1f} s)" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 89, + "descricao": ( + "Temperatura em nível de emergência, aguardando " + "confirmação da leitura" + ), + "acoes": [ + "Acompanhar imediatamente as próximas leituras.", + "Preparar a interrupção do esforço dos motores.", + ], + }) + + # Ainda não bloqueia, pois não passou pela confirmação. + penalidade = 45 + + # ============================================================ + # FALHA TÉRMICA CONFIRMADA + # ============================================================ + + elif estado_temp_falha["nivel"] == "confirmado": + motivos.append( + f"Temperatura perigosa do {nome_origem} confirmada: " + f"{temperatura:.1f} °C" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 95, + "descricao": ( + "Temperatura perigosa persistiu por " + f"{estado_temp_falha['duracao_s']:.1f} s" + ), + "acoes": [ + "Interromper ou reduzir o esforço dos motores.", + "Verificar a ventilação do compartimento.", + "Inspecionar motor, driver e sensor térmico.", + ], + }) + + penalidade = 80 + risco_eletrico_termico = True + + # ============================================================ + # RECUPERANDO DE FALHA TÉRMICA + # ============================================================ + + elif estado_temp_falha["nivel"] == "recuperando": + motivos.append( + f"Temperatura do {nome_origem} normalizando após condição " + f"perigosa: {temperatura:.1f} °C. " + "Aguardando estabilidade para liberação." + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 95, + "descricao": ( + "Temperatura caiu abaixo do limite de recuperação da " + "falha, mas ainda aguarda estabilidade por " + f"{self.TEMP_RECUPERACAO_S:.1f} s " + f"({estado_temp_falha['duracao_s']:.1f} s decorridos)" + ), + "acoes": [ + "Manter a carga reduzida.", + "Aguardar a conclusão da recuperação térmica.", + "Verificar se a temperatura permanece estável.", + ], + }) + + # Continua bloqueante até terminar a recuperação. + penalidade = 80 + risco_eletrico_termico = True + + # ============================================================ + # FALHA TÉRMICA PERSISTINDO + # ============================================================ + + elif estado_temp_falha["nivel"] == "persistente": + motivos.append( + f"Temperatura perigosa do {nome_origem} persistindo: " + f"{temperatura:.1f} °C " + f"({estado_temp_falha['duracao_s']:.1f} s)" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 85, + "descricao": ( + "Temperatura perigosa persistindo, aguardando " + "confirmação de segurança" + ), + "acoes": [ + "Acompanhar as próximas leituras.", + "Reduzir a carga se a temperatura continuar aumentando.", + ], + }) + + penalidade = 35 + + # ============================================================ + # PRIMEIRA DETECÇÃO DE FALHA TÉRMICA + # ============================================================ + + elif estado_temp_falha["nivel"] == "detectado": + motivos.append( + f"Pico de temperatura perigosa do {nome_origem} detectado: " + f"{temperatura:.1f} °C" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 70, + "descricao": ( + "Temperatura perigosa detectada, ainda sem persistência" + ), + "acoes": [ + "Acompanhar a confirmação da leitura térmica.", + ], + }) + + penalidade = 10 + + # ============================================================ + # ALERTA TÉRMICO CONFIRMADO + # ============================================================ + + elif estado_temp_alerta["nivel"] == "confirmado": + motivos.append( + f"Temperatura elevada do {nome_origem} confirmada: " + f"{temperatura:.1f} °C" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 60, + "descricao": ( + "Temperatura elevada persistiu por " + f"{estado_temp_alerta['duracao_s']:.1f} s" + ), + "acoes": [ + "Reduzir a carga dos motores.", + "Verificar a ventilação.", + ], + }) + + penalidade = 25 + + # ============================================================ + # RECUPERANDO DE ALERTA TÉRMICO + # ============================================================ + + elif estado_temp_alerta["nivel"] == "recuperando": + motivos.append( + f"Temperatura do {nome_origem} normalizando após alerta: " + f"{temperatura:.1f} °C" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 60, + "descricao": ( + "Temperatura caiu abaixo do limite de recuperação do " + "alerta, aguardando estabilidade por " + f"{self.TEMP_RECUPERACAO_S:.1f} s " + f"({estado_temp_alerta['duracao_s']:.1f} s decorridos)" + ), + "acoes": [ + "Acompanhar a estabilização da temperatura.", + "Manter a carga moderada durante a recuperação.", + ], + }) + + # Mantém a penalidade do alerta até concluir a recuperação, + # mas não caracteriza risco elétrico/térmico bloqueante. + penalidade = 25 + + # ============================================================ + # ALERTA PERSISTINDO + # ============================================================ + + elif estado_temp_alerta["nivel"] == "persistente": + motivos.append( + f"Aquecimento do {nome_origem} persistindo: " + f"{temperatura:.1f} °C " + f"({estado_temp_alerta['duracao_s']:.1f} s)" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 50, + "descricao": ( + "Temperatura elevada persistindo, aguardando confirmação" + ), + "acoes": [ + "Acompanhar a evolução da temperatura.", + "Verificar a ventilação se continuar aumentando.", + ], + }) + + penalidade = 15 + + # ============================================================ + # PRIMEIRA DETECÇÃO DE ALERTA + # ============================================================ + + elif estado_temp_alerta["nivel"] == "detectado": + motivos.append( + f"Aquecimento do {nome_origem} detectado: " + f"{temperatura:.1f} °C" + ) + + condicoes.append({ + "label": label, + "valor": temperatura, + "severidade": 40, + "descricao": ( + "Temperatura acima da faixa ideal, aguardando persistência" + ), + "acoes": [ + "Acompanhar a evolução da temperatura.", + ], + }) + + penalidade = 5 + + return ( + penalidade, + motivos, + condicoes, + risco_eletrico_termico, + ) + # ============================================================ # Frequência / contrato # ============================================================ @@ -835,16 +1258,13 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): @staticmethod def _penalizar_frequencia(freq, base, minimo, max_penalidade): - try: - return float(penalizar_frequencia(freq, base, minimo, max_penalidade) or 0) - except Exception: - if base <= 0: - return 0.0 - if freq >= minimo: - return 0.0 - if freq <= 0: - return float(max_penalidade) - return float(max_penalidade) * (1.0 - max(0.0, min(1.0, freq / max(minimo, 0.001)))) + if base <= 0: + return 0.0 + if freq >= minimo: + return 0.0 + if freq <= 0: + return float(max_penalidade) + return float(max_penalidade) * (1.0 - max(0.0, min(1.0, freq / max(minimo, 0.001)))) # ============================================================ # Padrões dinâmicos @@ -903,6 +1323,117 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): except Exception: pass + def _estado_persistencia( + self, + driver, + regra, + ativa, + tempo_intermediario_s, + tempo_confirmacao_s, + tempo_recuperacao_s=0.0, + ): + agora = time.monotonic() + chave = (str(driver), str(regra)) + + estado = self._estados_persistencia.setdefault(chave, { + "ativa_desde": None, + "normal_desde": None, + "confirmada": False, + }) + + if ativa: + estado["normal_desde"] = None + + if estado["ativa_desde"] is None: + estado["ativa_desde"] = agora + + duracao_s = max(0.0, agora - estado["ativa_desde"]) + + if duracao_s >= tempo_confirmacao_s: + estado["confirmada"] = True + + if estado["confirmada"]: + nivel = "confirmado" + elif duracao_s >= tempo_intermediario_s: + nivel = "persistente" + else: + nivel = "detectado" + + return { + "ativa": True, + "nivel": nivel, + "duracao_s": duracao_s, + "confirmada": estado["confirmada"], + } + + estado["ativa_desde"] = None + + if not estado["confirmada"]: + estado["normal_desde"] = None + return { + "ativa": False, + "nivel": "normal", + "duracao_s": 0.0, + "confirmada": False, + } + + if estado["normal_desde"] is None: + estado["normal_desde"] = agora + + normal_ha_s = max(0.0, agora - estado["normal_desde"]) + + if normal_ha_s >= tempo_recuperacao_s: + estado["confirmada"] = False + estado["normal_desde"] = None + + return { + "ativa": False, + "nivel": "normal", + "duracao_s": 0.0, + "confirmada": False, + } + + return { + "ativa": True, + "nivel": "recuperando", + "duracao_s": normal_ha_s, + "confirmada": True, + } + + def _estado_limite_alto( + self, + driver, + regra, + valor, + limite_entrada, + limite_saida, + tempo_intermediario_s, + tempo_confirmacao_s, + tempo_recuperacao_s, + ): + chave = (str(driver), str(regra)) + persistencia = self._estados_persistencia.get(chave, {}) + confirmada = bool(persistencia.get("confirmada", False)) + + # Histerese: + # ainda não confirmou -> entra no limite_entrada + # já confirmou -> permanece até cair abaixo do limite_saida + ativa = ( + valor >= limite_saida + if confirmada + else valor >= limite_entrada + ) + + return self._estado_persistencia( + driver=driver, + regra=regra, + ativa=ativa, + tempo_intermediario_s=tempo_intermediario_s, + tempo_confirmacao_s=tempo_confirmacao_s, + tempo_recuperacao_s=tempo_recuperacao_s, + ) + + # ============================================================ # Status/publicação # ============================================================ diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/pc.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/pc.py index cc76f99c6..0f6c4dcc2 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/pc.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/pc.py @@ -148,7 +148,7 @@ class ModuloPC(ModuloDiagnosticoBase): # Processo C# "process_private_mem_warn_mb": 6000.0, "process_private_mem_fail_mb": 10000.0, - "process_threads_warn": 120, + "process_threads_warn": 160, "process_threads_fail": 220, # ThreadPool @@ -615,19 +615,6 @@ class ModuloPC(ModuloDiagnosticoBase): ], )) - if st_gpu_mem_load != StatusModulo.OPERANTE: - gpu_cond.append(self._cond( - "Carga da Memória do GPU", - raw["gpu_memory_load_percent"], - 80, - "Controlador/memória da GPU sob carga elevada.", - [ - "Verificar tamanho dos tensores", - "Reduzir cópias CPU/GPU", - "Avaliar TensorRT/ONNX", - ], - )) - if gpu_power is not None and gpu_power >= C["gpu_power_warn_w"]: gpu_cond.append(self._cond( "Potência do GPU", @@ -654,6 +641,23 @@ class ModuloPC(ModuloDiagnosticoBase): else StatusModulo.OPERANTE ) + if st_gpu_memory_block != StatusModulo.OPERANTE: + gpu_memory_value = max( + x for x in [gpu_mem_load, gpu_fb_load] + if x is not None + ) if gpu_mem_load is not None or gpu_fb_load is not None else None + gpu_cond.append(self._cond( + "Carga da Memória do GPU", + gpu_memory_value, + 80, + "Controlador/memória da GPU sob carga elevada.", + [ + "Verificar tamanho dos tensores", + "Reduzir cópias CPU/GPU", + "Avaliar TensorRT/ONNX", + ], + )) + saude_individual.append(self._item( "gpu_temp", "Temperatura do GPU", @@ -692,7 +696,7 @@ class ModuloPC(ModuloDiagnosticoBase): "Carga de Memória/Framebuffer GPU", st_gpu_memory_block, sc_gpu_memory_block, - self._motivo("gpu_memory_load", st_gpu_memory_block, min( + self._motivo("gpu_memory_load", st_gpu_memory_block, max( x for x in [gpu_mem_load, gpu_fb_load] if x is not None ) if gpu_mem_load is not None or gpu_fb_load is not None else None, "%"), [c for c in gpu_cond if c["label"] == "Carga da Memória do GPU"], @@ -877,29 +881,78 @@ class ModuloPC(ModuloDiagnosticoBase): ) sc_process = min(sc_proc_mem, sc_proc_threads, sc_tp) - st_process = ( - StatusModulo.FALHA - if StatusModulo.FALHA in [st_proc_mem, st_proc_threads, st_tp] - else StatusModulo.ALERTA - if StatusModulo.ALERTA in [st_proc_mem, st_proc_threads, st_tp] - else StatusModulo.OPERANTE - ) + status_processos = [st_proc_mem, st_proc_threads, st_tp] + + if StatusModulo.FALHA in status_processos: + st_process = StatusModulo.FALHA + elif StatusModulo.ALERTA in status_processos: + st_process = StatusModulo.ALERTA + elif StatusModulo.DESCONECTADO in status_processos: + st_process = StatusModulo.DESCONECTADO + else: + st_process = StatusModulo.OPERANTE process_cond = [] - if st_process != StatusModulo.OPERANTE: + process_motivos = [] + + if st_proc_mem != StatusModulo.OPERANTE: + process_motivos += self._motivo( + "process_memory", + st_proc_mem, + proc_private, + "MB", + ) process_cond.append(self._cond( - "Processo C#", + "Memória do Processo", raw["process_private_memory_mb"], - 100 if st_process == StatusModulo.FALHA else 70, - "Processo principal com uso elevado de memória, threads ou ThreadPool.", + 100 if st_proc_mem == StatusModulo.FALHA else 70, + "Uso elevado de memória privada pelo processo principal.", [ - "Verificar vazamento de memória", - "Revisar criação de threads", - "Verificar tasks longas no C#", + "Verificar crescimento contínuo da memória", + "Verificar retenção de imagens e buffers", + "Verificar objetos gráficos não descartados", ], )) - motivos += self._motivo("process", st_process, proc_private, "MB") + if st_proc_threads != StatusModulo.OPERANTE: + process_motivos += self._motivo( + "process_threads", + st_proc_threads, + proc_threads, + "", + ) + process_cond.append(self._cond( + "Threads do Processo", + raw["process_thread_count"], + 100 if st_proc_threads == StatusModulo.FALHA else 70, + "Quantidade elevada de threads no processo principal.", + [ + "Verificar criação recorrente de threads e timers", + "Verificar ciclos de reconexão de dispositivos", + "Verificar threads que não estão sendo finalizadas", + ], + )) + + if st_tp != StatusModulo.OPERANTE: + process_motivos += self._motivo( + "threadpool_worker", + st_tp, + tp_worker, + "%", + ) + process_cond.append(self._cond( + "Uso do ThreadPool", + raw["threadpool_used_worker_percent"], + 100 if st_tp == StatusModulo.FALHA else 70, + "Ocupação elevada das threads worker do ThreadPool.", + [ + "Verificar tarefas bloqueantes", + "Verificar operações síncronas demoradas", + "Verificar tasks de longa duração", + ], + )) + + motivos += process_motivos condicoes += process_cond saude_individual.append(self._item( @@ -907,10 +960,10 @@ class ModuloPC(ModuloDiagnosticoBase): "Processo Principal", st_process, sc_process, - self._motivo("process", st_process, proc_private, "MB"), + process_motivos, process_cond, - valor=raw["process_private_memory_mb"], - unidade="MB", + valor=None, + unidade="", )) # ====================================================== @@ -979,17 +1032,21 @@ class ModuloPC(ModuloDiagnosticoBase): st_ram == StatusModulo.FALHA, st_vram == StatusModulo.FALHA, st_disk == StatusModulo.FALHA, + st_process == StatusModulo.FALHA, ]) - any_alert = any( - item.get("status") == StatusModulo.ALERTA.value + any_problem = any( + item.get("status") in ( + StatusModulo.ALERTA.value, + StatusModulo.FALHA.value, + ) for item in saude_individual ) if hard_fail: status = StatusModulo.FALHA score_total = min(score_total, 35.0) - elif score_total < 80.0 or any_alert: + elif score_total < 80.0 or any_problem: status = StatusModulo.ALERTA else: status = StatusModulo.OPERANTE