Compare commits
2 Commits
fce03f5044
...
bb546da70f
| Author | SHA1 | Date |
|---|---|---|
|
|
bb546da70f | |
|
|
bfed826136 |
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ============================================================
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -296,6 +296,8 @@ class WeedDetector:
|
|||
|
||||
cfg = self.config
|
||||
vel_mps = float(cfg.get("velocidade_robo", 0.0) or 0.0)
|
||||
# A geometria de atuação foi definida para o avanço. A ré não é
|
||||
# reinterpretada como um percurso espacial inverso dos bicos.
|
||||
if vel_mps < 0:
|
||||
vel_mps = 0.0
|
||||
|
||||
|
|
@ -463,9 +465,14 @@ class WeedDetector:
|
|||
self._frac_grid_last = frac_grid
|
||||
|
||||
# 3) Atualiza score espacial.
|
||||
# Em movimento, acumula evidência e desloca a esteira normalmente.
|
||||
# Parado, acompanha a evidência do frame atual por EMA: o mesmo ruído
|
||||
# não pode ser somado indefinidamente, mas uma erva persistente continua
|
||||
# podendo manter/ligar o bico.
|
||||
score_update_info = self._atualizar_score_memoria(
|
||||
frac_grid=frac_grid,
|
||||
cfg=cfg,
|
||||
em_movimento=bool(shift_info["em_movimento"]),
|
||||
)
|
||||
|
||||
# 4) Decide bicos pela zona física de atuação, com compensação de latência.
|
||||
|
|
@ -533,6 +540,7 @@ class WeedDetector:
|
|||
return {
|
||||
"dt_s": 0.0,
|
||||
"vel_mps": float(vel_mps),
|
||||
"em_movimento": False,
|
||||
"delta_frac": 0.0,
|
||||
"shift_cells": 0.0,
|
||||
"distancia_deslocada_total_cm": float(self._distancia_deslocada_total_cm),
|
||||
|
|
@ -545,10 +553,17 @@ class WeedDetector:
|
|||
if max_dt > 0:
|
||||
dt = min(dt, max_dt)
|
||||
|
||||
if dt <= 0 or vel_mps <= 0:
|
||||
vel_min = max(
|
||||
0.0,
|
||||
float(cfg.get("vel_min_movimento_memoria_mps", 0.03) or 0.03),
|
||||
)
|
||||
em_movimento = bool(abs(vel_mps) >= vel_min)
|
||||
|
||||
if dt <= 0 or not em_movimento:
|
||||
return {
|
||||
"dt_s": float(max(0.0, dt)),
|
||||
"vel_mps": float(vel_mps),
|
||||
"em_movimento": False,
|
||||
"delta_frac": 0.0,
|
||||
"shift_cells": 0.0,
|
||||
"distancia_deslocada_total_cm": float(self._distancia_deslocada_total_cm),
|
||||
|
|
@ -582,6 +597,7 @@ class WeedDetector:
|
|||
return {
|
||||
"dt_s": float(dt),
|
||||
"vel_mps": float(vel_mps),
|
||||
"em_movimento": True,
|
||||
"delta_frac": float(delta_frac),
|
||||
"shift_cells": float(shift_cells),
|
||||
"k_shift_frame_por_mps": float(k_shift),
|
||||
|
|
@ -723,6 +739,7 @@ class WeedDetector:
|
|||
self,
|
||||
frac_grid: np.ndarray,
|
||||
cfg: dict,
|
||||
em_movimento: bool = True,
|
||||
):
|
||||
min_frac_pred = float(
|
||||
cfg.get(
|
||||
|
|
@ -748,14 +765,35 @@ class WeedDetector:
|
|||
evidencia = np.clip(frac_grid / percent_ref, 0.0, 1.0).astype(np.float32)
|
||||
cond_pos = frac_grid >= min_frac_pred
|
||||
|
||||
# Decaimento geral, depois reforço positivo.
|
||||
score = np.maximum(0.0, self._score_grid - down_decay)
|
||||
if em_movimento:
|
||||
# Em movimento, cada frame traz evidência de uma nova faixa física
|
||||
# do terreno; mantém a estratégia acumulativa original.
|
||||
score = np.maximum(0.0, self._score_grid - down_decay)
|
||||
|
||||
score = np.where(
|
||||
cond_pos,
|
||||
np.minimum(1.0, score + up_gain * evidencia),
|
||||
score,
|
||||
).astype(np.float32)
|
||||
score = np.where(
|
||||
cond_pos,
|
||||
np.minimum(1.0, score + up_gain * evidencia),
|
||||
score,
|
||||
).astype(np.float32)
|
||||
modo_atualizacao = "acumulativo_movimento"
|
||||
else:
|
||||
# Parado, vários frames observam essencialmente o mesmo terreno.
|
||||
# Uma EMA faz o score convergir para a evidência atual, em vez de
|
||||
# transformar repetição temporal em confiança espacial infinita.
|
||||
alpha_parado = float(cfg.get("score_alpha_parado", 0.70) or 0.70)
|
||||
alpha_parado = float(np.clip(alpha_parado, 0.0, 0.99))
|
||||
|
||||
evidencia_atual = np.where(
|
||||
cond_pos,
|
||||
evidencia,
|
||||
0.0,
|
||||
).astype(np.float32)
|
||||
|
||||
score = (
|
||||
alpha_parado * self._score_grid
|
||||
+ (1.0 - alpha_parado) * evidencia_atual
|
||||
).astype(np.float32)
|
||||
modo_atualizacao = "ema_parado"
|
||||
|
||||
self._score_grid = score
|
||||
|
||||
|
|
@ -764,6 +802,11 @@ class WeedDetector:
|
|||
"percent_ref_evidencia": float(percent_ref),
|
||||
"score_up_gain": float(up_gain),
|
||||
"score_down_decay": float(down_decay),
|
||||
"em_movimento": bool(em_movimento),
|
||||
"modo_atualizacao": modo_atualizacao,
|
||||
"score_alpha_parado": float(
|
||||
cfg.get("score_alpha_parado", 0.70) or 0.70
|
||||
),
|
||||
"frac_grid_max": float(frac_grid.max()) if frac_grid.size else 0.0,
|
||||
"evidencia_max": float(evidencia.max()) if evidencia.size else 0.0,
|
||||
}
|
||||
|
|
@ -788,20 +831,32 @@ class WeedDetector:
|
|||
|
||||
k_shift = max(0.0, k_shift)
|
||||
|
||||
# Antecipação em fração da imagem:
|
||||
# Se demora para atuar, avalia células um pouco acima da zona real,
|
||||
# porque elas chegarão na zona quando o bico abrir de fato.
|
||||
antecipacao_frac = max(0.0, vel_mps * lat_s * k_shift)
|
||||
# A zona configurada representa a posição física fixa dos bicos.
|
||||
# Com faixa=0,85, área=0,10 e inversão vertical, 85%..95% da imagem
|
||||
# original vira 5%..15% na convenção interna. Essa zona NÃO muda de
|
||||
# parado até a velocidade de referência. Somente a parcela de
|
||||
# velocidade ACIMA da referência antecipa a avaliação para compensar
|
||||
# o tempo de resposta do sistema.
|
||||
vel_referencia = max(
|
||||
0.0,
|
||||
float(cfg.get("velocidade_referencia_atuacao_mps", 0.65) or 0.65),
|
||||
)
|
||||
velocidade_abs = abs(float(vel_mps))
|
||||
velocidade_excedente = max(0.0, velocidade_abs - vel_referencia)
|
||||
antecipacao_extra_frac = velocidade_excedente * lat_s * k_shift
|
||||
|
||||
if getattr(self, "inverter_sentido_vertical_bicos", False):
|
||||
# Se a erva vem do pé para o topo, antecipar significa olhar
|
||||
# um pouco abaixo da zona real, pois ela ainda vai subir até o bico.
|
||||
zona_eval_ini_frac = zona_ini_frac + antecipacao_frac
|
||||
zona_eval_fim_frac = zona_fim_frac + antecipacao_frac
|
||||
else:
|
||||
# Lógica atual: erva vem do topo para o pé.
|
||||
zona_eval_ini_frac = zona_ini_frac - antecipacao_frac
|
||||
zona_eval_fim_frac = zona_fim_frac - antecipacao_frac
|
||||
# A posição dos bicos é fixa. Não tentamos reinterpretar essa zona na
|
||||
# marcha a ré; a antecipação só indica uma região anterior no sentido
|
||||
# visual configurado para a operação normal de avanço.
|
||||
sentido_vertical = (
|
||||
1.0
|
||||
if getattr(self, "inverter_sentido_vertical_bicos", False)
|
||||
else -1.0
|
||||
)
|
||||
offset_assinado = antecipacao_extra_frac * sentido_vertical
|
||||
|
||||
zona_eval_ini_frac = zona_ini_frac + offset_assinado
|
||||
zona_eval_fim_frac = zona_fim_frac + offset_assinado
|
||||
|
||||
zona_eval_ini_frac = max(0.0, min(1.0, zona_eval_ini_frac))
|
||||
zona_eval_fim_frac = max(0.0, min(1.0, zona_eval_fim_frac))
|
||||
|
|
@ -872,7 +927,12 @@ class WeedDetector:
|
|||
|
||||
# Zona avaliada com compensação de latência.
|
||||
"latencia_total_atuacao_ms": float(lat_ms),
|
||||
"antecipacao_frac": float(antecipacao_frac),
|
||||
"antecipacao_frac": float(antecipacao_extra_frac),
|
||||
"velocidade_referencia_atuacao_mps": float(vel_referencia),
|
||||
"velocidade_excedente_atuacao_mps": float(velocidade_excedente),
|
||||
"offset_base_referencia_frac": 0.0,
|
||||
"antecipacao_extra_frac": float(antecipacao_extra_frac),
|
||||
"offset_assinado_frac": float(offset_assinado),
|
||||
"zona_eval_inicio_frac": float(zona_eval_ini_frac),
|
||||
"zona_eval_fim_frac": float(zona_eval_fim_frac),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue