diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/main.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/main.py index e851a1f12..3cbc60b89 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/main.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/main.py @@ -30,7 +30,7 @@ def main(): T_Code.Sen: ModuloSensoriamento(), T_Code.Atu: ModuloAtuador(), #T_Code.Lra: ModuloLoRa(), - T_Code.Imu: CameraIMU(modulos_imu=[T_Code.Snr, T_Code.Cam], max_disagreement_deg=4.0, max_skew_imu_s=0.12), + T_Code.Imu: CameraIMU(modulos_imu=[T_Code.Snr]), # CameraIMU(modulos_imu=[T_Code.Snr, T_Code.Cam], max_disagreement_deg=4.0, max_skew_imu_s=0.12), T_Code.Npc: ModuloPC(), T_Code.Lvx: ModuloLivox(), T_Code.Ipb: ModuloIPBribge(), 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 0ee79d1b3..f15dde509 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 @@ -1,4 +1,6 @@ import time +import math +from collections import deque from shared.enums import T_Code, StatusModulo from health_worker.modulos.base import ModuloDiagnosticoBase from health_worker.aprendizado.padrao_dinamico import PadraoDinamico @@ -75,7 +77,6 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): TEMP_AMOSTRAS_MIN_FALHA = 3 TEMP_AMOSTRAS_MIN_EMERGENCIA = 3 TEMP_AMOSTRA_MAX_IDADE_S = 3.0 - TEMP_SALTO_SUSPEITO_C = 35.0 # Elétricos TENSAO_FALHA_PERSISTENCIA_S = 2.0 @@ -94,6 +95,21 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): CORRENTE_MOTOR_EXTREMA_A = 90.0 CORRENTE_BARRAMENTO_EXTREMA_A = 55.0 + TEMP_MIN_FISICA_C = 0.0 + TEMP_MAX_FISICA_C = 150.0 + + # Uma elevação maior que isso entre amostras próximas entra em quarentena. + TEMP_SALTO_SUSPEITO_C = 12.0 + + # Velocidade térmica aceitável. O piso de 3 °C tolera ruído e quantização. + TEMP_VARIACAO_BASE_C = 3.0 + TEMP_SUBIDA_MAX_C_S = 2.0 + TEMP_DESCIDA_MAX_C_S = 4.0 + + # Para aceitar um novo patamar após salto. + TEMP_CONFIRMAR_PATAMAR_AMOSTRAS = 3 + TEMP_CONFIRMAR_PATAMAR_TOL_C = 5.0 + def __init__(self): self.t_code = T_Code.Mov self.nome = "Movimentacao" @@ -140,6 +156,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): self._estados_persistencia = {} self._historico_amostras = {} + self._estados_temperatura = {} def atualizar_saude(self): try: @@ -979,8 +996,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): def _temperatura_valida(self, temperatura): try: valor = float(temperatura) - return 0.0 < valor < 200.0 - except (TypeError, ValueError): + + return ( + math.isfinite(valor) + and self.TEMP_MIN_FISICA_C < valor < self.TEMP_MAX_FISICA_C + ) + except (TypeError, ValueError, OverflowError): return False def _avaliar_temperatura( @@ -1005,61 +1026,76 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): label = f"Temperatura do {nome_origem}" amostra = self._registrar_amostra(endereco, campo, temperatura, modulo) + normalizada = self._normalizar_temperatura(endereco=endereco, campo=campo, temperatura=temperatura, amostra=amostra) - if not self._temperatura_valida(temperatura): + if normalizada["status"] == "invalida": motivos.append( - f"Temperatura do {nome_origem} inválida: " - f"{temperatura} °C" + f"Temperatura do {nome_origem} inválida: {temperatura}" ) - + condicoes.append({ "label": label, "valor": temperatura, "severidade": 50, "descricao": ( - "Leitura de temperatura inválida" + "Amostra térmica inválida; ignorada pela proteção " + "de sobretemperatura" ), "acoes": [ - "Verificar sensor de temperatura.", + "Verificar sensor, conversão, aterramento e cabeamento.", ], }) - return ( - penalidade, - motivos, - condicoes, - risco_eletrico_termico, + + self._resetar_candidatos_temperatura(endereco, regra_desc) + + return self._resultado_temperatura_inconclusiva(endereco, regra_desc, motivos, condicoes) + + if normalizada["status"] == "desatualizada": + motivos.append( + f"Temperatura do {nome_origem} desatualizada; " + "sobretemperatura não confirmada" ) - if not amostra["fresca"]: + return self._resultado_temperatura_inconclusiva(endereco, regra_desc, motivos, condicoes) + + if normalizada["status"] == "suspeita": motivos.append( - f"Temperatura do {nome_origem} sem amostra fresca " - f"({amostra['idade_s']:.1f} s)" + f"Amostra térmica suspeita ignorada no {nome_origem}: " + f"{float(temperatura):.1f} °C; " + f"referência validada={normalizada['valor_validado']:.1f} °C; " + f"delta={normalizada['delta_c']:+.1f} °C; " + f"confirmações=" + f"{normalizada['amostras_candidatas']}/" + f"{self.TEMP_CONFIRMAR_PATAMAR_AMOSTRAS}" ) + condicoes.append({ "label": label, "valor": temperatura, - "severidade": 55, - "descricao": "Leitura térmica está congelada ou desatualizada; não confirma sobretemperatura", - "acoes": ["Verificar publicação e timestamp do sensor térmico."], + "severidade": 35, + "descricao": ( + "Salto térmico fisicamente implausível em quarentena; " + "não classificado como temperatura elevada" + ), + "acoes": [ + "Aguardar as próximas amostras.", + "Verificar sensor e aterramento se o ruído persistir.", + ], }) - return (0, motivos, condicoes, False) - if amostra["salto_suspeito"]: - motivos.append( - f"Salto térmico suspeito no {nome_origem}: {amostra['delta_c']:.1f} °C" - ) - condicoes.append({ - "label": label, - "valor": temperatura, - "severidade": 70, - "descricao": "Variação térmica instantânea acima do fisicamente plausível; aguardando confirmação", - "acoes": ["Verificar sensor, divisor, aterramento e próximas amostras."], - }) + self._resetar_candidatos_temperatura(endereco, regra_desc) + + return self._resultado_temperatura_inconclusiva(endereco, regra_desc, motivos, condicoes) + + if normalizada["status"] == "repetida": + return self._resultado_temperatura_inconclusiva(endereco, regra_desc, motivos, condicoes) + + temperatura_avaliada = normalizada["valor_validado"] estado_temp_emergencia = self._estado_limite_alto( driver=endereco, regra=f"temperatura_emergencia_{regra_desc}", - valor=temperatura, + valor=temperatura_avaliada, limite_entrada=self.TEMP_EMERGENCIA_C, limite_saida=self.TEMP_RECUPERA_EMERGENCIA_C, tempo_intermediario_s=0.0, @@ -1072,7 +1108,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): estado_temp_falha = self._estado_limite_alto( driver=endereco, regra=f"temperatura_falha_{regra_desc}", - valor=temperatura, + valor=temperatura_avaliada, limite_entrada=self.TEMP_FALHA_C, limite_saida=self.TEMP_RECUPERA_FALHA_C, tempo_intermediario_s=self.TEMP_FALHA_INTERMEDIARIO_S, @@ -1085,7 +1121,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): estado_temp_alerta = self._estado_limite_alto( driver=endereco, regra=f"temperatura_alerta_{regra_desc}", - valor=temperatura, + valor=temperatura_avaliada, limite_entrada=self.TEMP_ALERTA_C, limite_saida=self.TEMP_RECUPERA_ALERTA_C, tempo_intermediario_s=self.TEMP_ALERTA_INTERMEDIARIO_S, @@ -1102,12 +1138,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): if estado_temp_emergencia["nivel"] == "confirmado": motivos.append( f"Temperatura do {nome_origem} em nível de emergência " - f"confirmado: {temperatura:.1f} °C" + f"confirmado: {temperatura_avaliada:.1f} °C" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 98, "descricao": ( "Temperatura de emergência confirmada após " @@ -1129,12 +1165,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): 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." + f"{temperatura_avaliada:.1f} °C. Aguardando estabilidade para liberação." ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 98, "descricao": ( "Temperatura caiu abaixo do limite de recuperação da " @@ -1160,13 +1196,13 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_emergencia["ativa"]: motivos.append( f"Temperatura de emergência do {nome_origem} detectada: " - f"{temperatura:.1f} °C " + f"{temperatura_avaliada:.1f} °C " f"({estado_temp_emergencia['duracao_s']:.1f} s)" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 89, "descricao": ( "Temperatura em nível de emergência, aguardando " @@ -1188,12 +1224,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_falha["nivel"] == "confirmado": motivos.append( f"Temperatura perigosa do {nome_origem} confirmada: " - f"{temperatura:.1f} °C" + f"{temperatura_avaliada:.1f} °C" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 95, "descricao": ( "Temperatura perigosa persistiu por " @@ -1216,13 +1252,13 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_falha["nivel"] == "recuperando": motivos.append( f"Temperatura do {nome_origem} normalizando após condição " - f"perigosa: {temperatura:.1f} °C. " + f"perigosa: {temperatura_avaliada:.1f} °C. " "Aguardando estabilidade para liberação." ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 95, "descricao": ( "Temperatura caiu abaixo do limite de recuperação da " @@ -1248,13 +1284,13 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_falha["nivel"] == "persistente": motivos.append( f"Temperatura perigosa do {nome_origem} persistindo: " - f"{temperatura:.1f} °C " + f"{temperatura_avaliada:.1f} °C " f"({estado_temp_falha['duracao_s']:.1f} s)" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 85, "descricao": ( "Temperatura perigosa persistindo, aguardando " @@ -1275,12 +1311,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_falha["nivel"] == "detectado": motivos.append( f"Pico de temperatura perigosa do {nome_origem} detectado: " - f"{temperatura:.1f} °C" + f"{temperatura_avaliada:.1f} °C" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 70, "descricao": ( "Temperatura perigosa detectada, ainda sem persistência" @@ -1299,12 +1335,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_alerta["nivel"] == "confirmado": motivos.append( f"Temperatura elevada do {nome_origem} confirmada: " - f"{temperatura:.1f} °C" + f"{temperatura_avaliada:.1f} °C" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 60, "descricao": ( "Temperatura elevada persistiu por " @@ -1325,12 +1361,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_alerta["nivel"] == "recuperando": motivos.append( f"Temperatura do {nome_origem} normalizando após alerta: " - f"{temperatura:.1f} °C" + f"{temperatura_avaliada:.1f} °C" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 60, "descricao": ( "Temperatura caiu abaixo do limite de recuperação do " @@ -1355,13 +1391,13 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_alerta["nivel"] == "persistente": motivos.append( f"Aquecimento do {nome_origem} persistindo: " - f"{temperatura:.1f} °C " + f"{temperatura_avaliada:.1f} °C " f"({estado_temp_alerta['duracao_s']:.1f} s)" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 50, "descricao": ( "Temperatura elevada persistindo, aguardando confirmação" @@ -1381,12 +1417,12 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): elif estado_temp_alerta["nivel"] == "detectado": motivos.append( f"Aquecimento do {nome_origem} detectado: " - f"{temperatura:.1f} °C" + f"{temperatura_avaliada:.1f} °C" ) condicoes.append({ "label": label, - "valor": temperatura, + "valor": temperatura_avaliada, "severidade": 40, "descricao": ( "Temperatura acima da faixa ideal, aguardando persistência" @@ -1909,6 +1945,183 @@ class ModuloMovimentacao(ModuloDiagnosticoBase): if str(k).strip() } + def _normalizar_temperatura( + self, + endereco, + campo, + temperatura, + amostra, + ): + chave = (str(endereco), str(campo)) + + estado = self._estados_temperatura.setdefault(chave, { + "valor_validado": None, + "mono_validado": None, + "candidatas": deque( + maxlen=self.TEMP_CONFIRMAR_PATAMAR_AMOSTRAS + ), + }) + + if not self._temperatura_valida(temperatura): + estado["candidatas"].clear() + + return { + "status": "invalida", + "valor_validado": estado["valor_validado"], + "motivo": "valor fora da faixa física ou não finito", + } + + valor = float(temperatura) + + if not amostra["fresca"]: + return { + "status": "desatualizada", + "valor_validado": estado["valor_validado"], + "motivo": "amostra térmica sem frescor", + } + + # Polling repetido do mesmo snapshot não confirma tendência. + if not amostra["nova"]: + return { + "status": "repetida", + "valor_validado": estado["valor_validado"], + "motivo": "snapshot já avaliado", + } + + agora = time.monotonic() + anterior = estado["valor_validado"] + mono_anterior = estado["mono_validado"] + + # Primeira amostra fisicamente válida vira referência. + if anterior is None or mono_anterior is None: + estado["valor_validado"] = valor + estado["mono_validado"] = agora + estado["candidatas"].clear() + + return { + "status": "validada", + "valor_validado": valor, + "motivo": "primeira referência térmica válida", + } + + dt = max(0.001, agora - mono_anterior) + delta = valor - anterior + + limite_delta = ( + self.TEMP_VARIACAO_BASE_C + + ( + self.TEMP_SUBIDA_MAX_C_S + if delta >= 0 + else self.TEMP_DESCIDA_MAX_C_S + ) * dt + ) + + if abs(delta) <= limite_delta: + estado["valor_validado"] = valor + estado["mono_validado"] = agora + estado["candidatas"].clear() + + return { + "status": "validada", + "valor_validado": valor, + "delta_c": delta, + "dt_s": dt, + "motivo": "variação térmica plausível", + } + + # Salto implausível: guarda o possível novo patamar, + # mas ainda não o entrega à proteção térmica. + estado["candidatas"].append({ + "valor": valor, + "mono": agora, + }) + + candidatas = list(estado["candidatas"]) + + patamar_coerente = ( + len(candidatas) >= self.TEMP_CONFIRMAR_PATAMAR_AMOSTRAS + and ( + max(x["valor"] for x in candidatas) + - min(x["valor"] for x in candidatas) + ) <= self.TEMP_CONFIRMAR_PATAMAR_TOL_C + ) + + if patamar_coerente: + valor_confirmado = sum( + x["valor"] for x in candidatas + ) / len(candidatas) + + estado["valor_validado"] = valor_confirmado + estado["mono_validado"] = agora + estado["candidatas"].clear() + + return { + "status": "patamar_confirmado", + "valor_validado": valor_confirmado, + "delta_c": valor_confirmado - anterior, + "dt_s": dt, + "motivo": ( + "novo patamar confirmado por " + f"{self.TEMP_CONFIRMAR_PATAMAR_AMOSTRAS} amostras" + ), + } + + return { + "status": "suspeita", + "valor_validado": anterior, + "delta_c": delta, + "dt_s": dt, + "amostras_candidatas": len(candidatas), + "motivo": "salto térmico em quarentena", + } + + def _resetar_candidatos_temperatura(self, endereco, regra_desc): + for nivel in ("alerta", "falha", "emergencia"): + chave = ( + str(endereco), + f"temperatura_{nivel}_{regra_desc}", + ) + + estado = self._estados_persistencia.get(chave) + + if estado and not estado.get("confirmada", False): + estado["ativa_desde"] = None + estado["normal_desde"] = None + estado["amostras_ativas"] = 0 + + def _resultado_temperatura_inconclusiva( + self, + endereco, + regra_desc, + motivos, + condicoes, + ): + def obter_estado(nivel): + chave = ( + str(endereco), + f"temperatura_{nivel}_{regra_desc}", + ) + return self._estados_persistencia.get(chave, {}) + + emergencia = obter_estado("emergencia") + falha = obter_estado("falha") + alerta = obter_estado("alerta") + + nivel_emergencia = emergencia.get("nivel") + nivel_falha = falha.get("nivel") + nivel_alerta = alerta.get("nivel") + + if nivel_emergencia in ("confirmado", "recuperando"): + return (90, motivos, condicoes, True) + + if nivel_falha in ("confirmado", "recuperando"): + return (80, motivos, condicoes, True) + + if nivel_alerta in ("confirmado", "recuperando"): + return (25, motivos, condicoes, False) + + return (0, motivos, condicoes, False) + @staticmethod def _bool(valor, default=False): try: 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 2fbced1a3..e87d932bf 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 @@ -52,13 +52,15 @@ class RegrasTaticas: freio_necessario = freio_necessario or freio novos_motivos, freio = self._avaliar_oak_parada_por_bloqueio( - controle=controle + controle=controle, + operacao=operacao ) motivos.extend(novos_motivos) freio_necessario = freio_necessario or freio novos_motivos, freio = self._avaliar_imu_parada_por_inclinacao( - controle=controle + controle=controle, + operacao=operacao ) motivos.extend(novos_motivos) freio_necessario = freio_necessario or freio @@ -124,7 +126,7 @@ class RegrasTaticas: return motivos, freio_necessario - def _avaliar_oak_parada_por_bloqueio(self, controle: dict): + def _avaliar_oak_parada_por_bloqueio(self, controle: dict, operacao: dict): """ Bloqueia somente se a função de parada por bloqueio estiver ativa. @@ -132,6 +134,12 @@ class RegrasTaticas: Esses são auxílios, não garantias absolutas de segurança. """ + sonar_mandatorio = self._modulo_mandatorio(operacao, T_Code.Snr) + if not sonar_mandatorio: + self._resetar_persistencia("oak_indisponivel") + self._resetar_persistencia("oak_obstaculo") + return [], False + oak_parada_por_bloqueio = self._bool( controle.get("oak_parada_por_bloqueio", False) ) @@ -234,12 +242,10 @@ class RegrasTaticas: return [], False - def _avaliar_imu_parada_por_inclinacao(self, controle: dict): - imu_parada_por_inclinacao = self._bool( - controle.get("imu_parada_por_inclinacao", False) - ) - - if not imu_parada_por_inclinacao: + def _avaliar_imu_parada_por_inclinacao(self, controle: dict, operacao: dict): + protecao_ativa = self._bool(controle.get("imu_parada_por_inclinacao", False)) + imu_mandatorio = self._modulo_mandatorio(operacao, T_Code.Imu) + if not protecao_ativa or not imu_mandatorio: self._resetar_persistencia("imu_indisponivel") self._resetar_persistencia("imu_perigo") return [], False @@ -537,6 +543,30 @@ class RegrasTaticas: return idade_s <= max_idade_s, idade_ms + def _modulo_mandatorio(self, operacao: dict, t_code: T_Code) -> bool: + # Se o campo ainda não chegou ao Python, preserva a proteção atual. + if "modulos_mandatorios" not in operacao: + return True + + valores = operacao.get("modulos_mandatorios", []) or [] + + normalizados = set() + + for item in valores: + try: + if isinstance(item, dict): + item = item.get( + "modulo", + item.get("codigo", item.get("t_code")) + ) + + item = getattr(item, "value", item) + normalizados.add(int(item)) + except (TypeError, ValueError): + continue + + return int(t_code.value) in normalizados + # ============================================================ # Helpers genéricos seguros # ============================================================ 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 5c7cc41c4..f272c4484 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 @@ -130,7 +130,7 @@ WEED_DEFAULT_CONFIG = { # ============================================================ # Gate global: só libera bicos quando existe alvo suficiente no frame. "usar_radar_global_gate": True, - "inverter_ordem_bicos": True, + "inverter_ordem_bicos": False, "inverter_sentido_vertical_bicos": True, # Histerese global da fração de alvo no frame completo.