novas regras do mov e nao permitir parar operacao se o modulo nao for mandatorio nas regras taticas

This commit is contained in:
Diego Freitas 2026-08-13 21:21:05 -03:00
parent fce03f5044
commit bfed826136
4 changed files with 311 additions and 68 deletions

View File

@ -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(),

View File

@ -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:

View File

@ -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
# ============================================================

View File

@ -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.