ajustes finais timeout bateria e movimentacao
This commit is contained in:
parent
c306c8727b
commit
724391826b
|
|
@ -1749,16 +1749,16 @@ namespace AgroBase.Models.Modules
|
|||
|
||||
tmrRegistrador.SetInterval(_TaxaAmostragem);
|
||||
|
||||
//if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Atu) && (_VezesLeituraReconexao >= _TempoReconexaoComponentes))
|
||||
//{
|
||||
// Variaveis.OperacaoEmAndamento.DispAtu?.EnviarProtocolosConfiguracao();
|
||||
//
|
||||
// _VezesLeituraReconexao = 0;
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// _VezesLeituraReconexao++;
|
||||
//}
|
||||
if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Atu) && (_VezesLeituraReconexao >= _TempoReconexaoComponentes))
|
||||
{
|
||||
Variaveis.OperacaoEmAndamento.DispAtu?.EnviarProtocolosConfiguracao();
|
||||
|
||||
_VezesLeituraReconexao = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_VezesLeituraReconexao++;
|
||||
}
|
||||
|
||||
if (DadosLeitura.Conectado)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2313,16 +2313,16 @@ namespace AgroBase.Models.Modules
|
|||
|
||||
tmrRegistrador.SetInterval(_TaxaAmostragem);
|
||||
|
||||
//if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Sen) && (_VezesLeituraReconexao >= _TempoReconexaoComponentes))
|
||||
//{
|
||||
// Variaveis.OperacaoEmAndamento.DispSen?.EnviarProtocolosConfiguracao();
|
||||
//
|
||||
// _VezesLeituraReconexao = 0;
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// _VezesLeituraReconexao++;
|
||||
//}
|
||||
if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Sen) && (_VezesLeituraReconexao >= _TempoReconexaoComponentes))
|
||||
{
|
||||
Variaveis.OperacaoEmAndamento.DispSen?.EnviarProtocolosConfiguracao();
|
||||
|
||||
_VezesLeituraReconexao = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_VezesLeituraReconexao++;
|
||||
}
|
||||
|
||||
if (DadosLeitura.Conectado)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -631,16 +631,16 @@ namespace AgroBase.Services
|
|||
_ultimaConsultaPendente[idTx] = dado;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"[OID/TX] idTx=" +
|
||||
idTx.ToString("X2") +
|
||||
" idRx=" +
|
||||
idRx.ToString("X2") +
|
||||
" param=" +
|
||||
dado +
|
||||
" data=0F-" +
|
||||
((byte)dado).ToString("X2")
|
||||
);
|
||||
//Console.WriteLine(
|
||||
// "[OID/TX] idTx=" +
|
||||
// idTx.ToString("X2") +
|
||||
// " idRx=" +
|
||||
// idRx.ToString("X2") +
|
||||
// " param=" +
|
||||
// dado +
|
||||
// " data=0F-" +
|
||||
// ((byte)dado).ToString("X2")
|
||||
//);
|
||||
|
||||
EnviarComando(
|
||||
Dispositivo,
|
||||
|
|
@ -1350,14 +1350,14 @@ namespace AgroBase.Services
|
|||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"[OID/RX] idTx=" +
|
||||
mensagem.IdTx.ToString("X") +
|
||||
" idRx=" +
|
||||
mensagem.IdRx.ToString("X") +
|
||||
" data=" +
|
||||
BitConverter.ToString(mensagem.DataRx ?? new byte[0])
|
||||
);
|
||||
//Console.WriteLine(
|
||||
// "[OID/RX] idTx=" +
|
||||
// mensagem.IdTx.ToString("X") +
|
||||
// " idRx=" +
|
||||
// mensagem.IdRx.ToString("X") +
|
||||
// " data=" +
|
||||
// BitConverter.ToString(mensagem.DataRx ?? new byte[0])
|
||||
//);
|
||||
|
||||
OidParametros parametro =
|
||||
(OidParametros)mensagem.DataRx[1];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,51 @@ class ModuloBateria(ModuloDiagnosticoBase):
|
|||
else:
|
||||
return StatusModulo.FALHA
|
||||
|
||||
def _normalizar_timestamp_segundos(self, valor):
|
||||
"""
|
||||
Normaliza timestamps vindos do Redis para Unix epoch em segundos.
|
||||
|
||||
Aceita:
|
||||
- segundos Unix: 1782158000.123
|
||||
- milissegundos: 1782158000123
|
||||
- valor ausente/inválido: 0.0
|
||||
"""
|
||||
try:
|
||||
ts = float(valor or 0.0)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
if ts <= 0:
|
||||
return 0.0
|
||||
|
||||
if ts > 1_000_000_000_000:
|
||||
return ts / 1000.0
|
||||
|
||||
if ts > 1_000_000_000:
|
||||
return ts
|
||||
|
||||
return 0.0
|
||||
|
||||
def _calcular_idade_telemetria_s(self, dados):
|
||||
now = time.time()
|
||||
|
||||
momento = self._normalizar_timestamp_segundos(
|
||||
dados.get("timestamp")
|
||||
)
|
||||
|
||||
if momento <= 0:
|
||||
return 9999.0
|
||||
|
||||
dt = now - momento
|
||||
|
||||
if dt < 0 and dt > -5.0:
|
||||
return 0.0
|
||||
|
||||
if dt < -5.0:
|
||||
return 9999.0
|
||||
|
||||
return dt
|
||||
|
||||
def atualizar_saude(self):
|
||||
try:
|
||||
SAUDE_MIN_ALERTA = 80
|
||||
|
|
@ -26,7 +71,6 @@ class ModuloBateria(ModuloDiagnosticoBase):
|
|||
condicoes = []
|
||||
saude_individual = []
|
||||
|
||||
now = time.perf_counter()
|
||||
saude = 0
|
||||
|
||||
dados = ContextoGlobalRedis.get_modulo(self.t_code) or {}
|
||||
|
|
@ -36,8 +80,7 @@ class ModuloBateria(ModuloDiagnosticoBase):
|
|||
saude = 0
|
||||
motivos.append("Desconectado")
|
||||
else:
|
||||
momento = float(dados.get("timestamp") or 0.0)
|
||||
dt = now - momento if momento > 0 else 9999.0
|
||||
dt = self._calcular_idade_telemetria_s(dados)
|
||||
|
||||
# ------------ LEITURA DOS DADOS DA BATERIA ------------
|
||||
v_total = dados.get("tensao_total_v")
|
||||
|
|
@ -349,7 +392,7 @@ class ModuloBateria(ModuloDiagnosticoBase):
|
|||
"temp_min_c": locals().get("t_min", 0),
|
||||
"temp_max_c": locals().get("t_max", 0),
|
||||
"bms_life_raw": locals().get("bms_life", 0),
|
||||
"tempo_desde_ultimo_update_s": locals().get("dt", 0),
|
||||
"tempo_desde_ultimo_update_s": max(0, round(locals().get("dt", 9999.0), 3)),
|
||||
"qtd_alarmes_ativos": len(locals().get("alarmes_ativos", [])),
|
||||
"alarmes_ativos": locals().get("alarmes_ativos", []),
|
||||
"score_com": round(locals().get("score_com", 0), 1),
|
||||
|
|
|
|||
|
|
@ -1,217 +1,713 @@
|
|||
import time
|
||||
from shared.enums import T_Code, StatusModulo
|
||||
from shared.utils import penalizar_frequencia
|
||||
from health_worker.modulos.base import ModuloDiagnosticoBase
|
||||
from health_worker.aprendizado.padrao_dinamico import PadraoDinamico
|
||||
from shared.contexto_global_redis import ContextoGlobalRedis
|
||||
|
||||
class ModuloMovimentacaoPadroes():
|
||||
|
||||
class ModuloMovimentacaoPadroes:
|
||||
def __init__(self):
|
||||
self.padrao_tensao = PadraoDinamico("tensao", faixa_inicial=(30, 42))
|
||||
self.padrao_corrente_motor = PadraoDinamico("corrente_motor", faixa_inicial=(0, 45))
|
||||
self.padrao_corrente_barramento = PadraoDinamico("corrente_barramento", faixa_inicial=(0, 15))
|
||||
self.padrao_temperatura_motor = PadraoDinamico("temperatura_motor", faixa_inicial=(10, 60))
|
||||
self.padrao_temperatura_driver = PadraoDinamico("temperatura_driver", faixa_inicial=(10, 60))
|
||||
self.padrao_corrente_motor = PadraoDinamico("corrente_motor", faixa_inicial=(-45, 45))
|
||||
self.padrao_corrente_barramento = PadraoDinamico("corrente_barramento", faixa_inicial=(-10, 20))
|
||||
self.padrao_temperatura_motor = PadraoDinamico("temperatura_motor", faixa_inicial=(10, 70))
|
||||
self.padrao_temperatura_driver = PadraoDinamico("temperatura_driver", faixa_inicial=(10, 70))
|
||||
|
||||
|
||||
class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||
def __init__(self):
|
||||
self.t_code = T_Code.Mov
|
||||
self.nome = "Movimentacao"
|
||||
self.timeout = 3 # segundos
|
||||
self.timeout = 3
|
||||
self.padroes = {}
|
||||
|
||||
# Fallback caso o C# ainda não envie freq_base/freq_min/freq_uso por parâmetro.
|
||||
# As chaves aqui precisam bater com Redis:
|
||||
# freq.rpm, freq.tensao, freq.corrente_motor, etc.
|
||||
self.freq_defaults = {
|
||||
"rpm": {
|
||||
"label": "rpm",
|
||||
"base": 1.0,
|
||||
"min": 0.45,
|
||||
"peso": 1.25,
|
||||
"em_uso": True,
|
||||
},
|
||||
"corrente_motor": {
|
||||
"label": "corrente_motor",
|
||||
"base": 1.0,
|
||||
"min": 0.45,
|
||||
"peso": 1.15,
|
||||
"em_uso": True,
|
||||
},
|
||||
"corrente_barramento": {
|
||||
"label": "corrente_barramento",
|
||||
"base": 1.0,
|
||||
"min": 0.45,
|
||||
"peso": 1.15,
|
||||
"em_uso": True,
|
||||
},
|
||||
"tensao": {
|
||||
"label": "tensao",
|
||||
"base": 0.5,
|
||||
"min": 0.20,
|
||||
"peso": 0.85,
|
||||
"em_uso": True,
|
||||
},
|
||||
"temperatura": {
|
||||
"label": "temperatura",
|
||||
"base": 0.5,
|
||||
"min": 0.20,
|
||||
"peso": 0.85,
|
||||
"em_uso": True,
|
||||
},
|
||||
"erro": {
|
||||
"label": "erro",
|
||||
"base": 0.5,
|
||||
"min": 0.20,
|
||||
"peso": 0.80,
|
||||
"em_uso": True,
|
||||
},
|
||||
# Telemetrias úteis, mas não devem derrubar o módulo se ficarem lentas.
|
||||
"ciclo_trabalho": {
|
||||
"label": "ciclo_trabalho",
|
||||
"base": 0.5,
|
||||
"min": 0.20,
|
||||
"peso": 0.35,
|
||||
"em_uso": False,
|
||||
},
|
||||
"potencia": {
|
||||
"label": "potencia",
|
||||
"base": 0.5,
|
||||
"min": 0.20,
|
||||
"peso": 0.35,
|
||||
"em_uso": False,
|
||||
},
|
||||
"posicao_absoluta": {
|
||||
"label": "posicao_absoluta",
|
||||
"base": 0.2,
|
||||
"min": 0.08,
|
||||
"peso": 0.20,
|
||||
"em_uso": False,
|
||||
},
|
||||
"io_status": {
|
||||
"label": "io_status",
|
||||
"base": 0.2,
|
||||
"min": 0.08,
|
||||
"peso": 0.20,
|
||||
"em_uso": False,
|
||||
},
|
||||
}
|
||||
|
||||
self.oid_erros_penalidades = {
|
||||
0: 0,
|
||||
1: 100, # Sobretensão
|
||||
2: 80, # Subtensão
|
||||
3: 100, # Corrente máxima excedida
|
||||
4: 80, # Temperatura MOS
|
||||
5: 60, # Subtensão MCU
|
||||
6: 100, # Watchdog
|
||||
7: 60, # Erro SPI
|
||||
8: 100, # Flash danificado
|
||||
9: 30,
|
||||
10: 30,
|
||||
11: 30,
|
||||
12: 70,
|
||||
13: 100,
|
||||
14: 100,
|
||||
15: 10,
|
||||
16: 0,
|
||||
17: 0,
|
||||
18: 40,
|
||||
19: 20,
|
||||
}
|
||||
|
||||
def _status_por_score(self, score):
|
||||
if score >= 80:
|
||||
return StatusModulo.OPERANTE
|
||||
if score > 0:
|
||||
return StatusModulo.ALERTA
|
||||
return StatusModulo.FALHA
|
||||
|
||||
@staticmethod
|
||||
def _to_float(valor, default=0.0):
|
||||
try:
|
||||
if valor is None:
|
||||
return default
|
||||
return float(valor)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def _to_int(valor, default=0):
|
||||
try:
|
||||
if valor is None:
|
||||
return default
|
||||
return int(float(valor))
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def _to_bool(valor, default=False):
|
||||
if isinstance(valor, bool):
|
||||
return valor
|
||||
if valor is None:
|
||||
return default
|
||||
if isinstance(valor, (int, float)):
|
||||
return valor != 0
|
||||
if isinstance(valor, str):
|
||||
return valor.strip().lower() in ("1", "true", "sim", "yes", "on")
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def _as_dict(valor):
|
||||
return valor if isinstance(valor, dict) else {}
|
||||
|
||||
def _normalizar_timestamp_segundos(self, valor):
|
||||
try:
|
||||
ts = float(valor or 0.0)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
if ts <= 0:
|
||||
return 0.0
|
||||
|
||||
# Unix ms
|
||||
if ts > 1_000_000_000_000:
|
||||
return ts / 1000.0
|
||||
|
||||
# Unix s
|
||||
if ts > 1_000_000_000:
|
||||
return ts
|
||||
|
||||
# Valor relativo/perf_counter ou inválido para comparar com time.time.
|
||||
return 0.0
|
||||
|
||||
def _idade_modulo_s(self, modulo):
|
||||
ts = self._normalizar_timestamp_segundos(modulo.get("timestamp"))
|
||||
if ts <= 0:
|
||||
return None
|
||||
|
||||
dt = time.time() - ts
|
||||
|
||||
# Pequena diferença de relógio não deve gerar alerta.
|
||||
if -5.0 < dt < 0:
|
||||
return 0.0
|
||||
|
||||
if dt < -5.0:
|
||||
return None
|
||||
|
||||
return dt
|
||||
|
||||
def _listar_ativos(self, dados_mov):
|
||||
ativos = dados_mov.get("ativos", [])
|
||||
|
||||
if isinstance(ativos, list) and ativos:
|
||||
return [str(x) for x in ativos]
|
||||
|
||||
# Fallback legado: percorre chaves numéricas.
|
||||
resultado = []
|
||||
for chave, valor in dados_mov.items():
|
||||
if isinstance(valor, dict):
|
||||
try:
|
||||
int(chave)
|
||||
resultado.append(str(chave))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return resultado
|
||||
|
||||
def _ler_freq_config(self, modulo, parametro):
|
||||
freqs = self._as_dict(modulo.get("freq"))
|
||||
freq_base_map = self._as_dict(modulo.get("freq_base"))
|
||||
freq_min_map = self._as_dict(modulo.get("freq_min"))
|
||||
freq_uso_map = self._as_dict(modulo.get("freq_uso"))
|
||||
|
||||
default = self.freq_defaults.get(
|
||||
parametro,
|
||||
{
|
||||
"label": parametro,
|
||||
"base": 0.5,
|
||||
"min": 0.2,
|
||||
"peso": 0.3,
|
||||
"em_uso": False,
|
||||
},
|
||||
)
|
||||
|
||||
freq_real = self._to_float(freqs.get(parametro), 0.0)
|
||||
|
||||
# Compatibilidade com campo antigo freq_base escalar.
|
||||
freq_base_bruto = modulo.get("freq_base", None)
|
||||
freq_base_legado = self._to_float(freq_base_bruto, default["base"])
|
||||
|
||||
if isinstance(freq_base_bruto, dict):
|
||||
freq_base_legado = default["base"]
|
||||
|
||||
base = self._to_float(
|
||||
freq_base_map.get(parametro),
|
||||
freq_base_legado,
|
||||
)
|
||||
|
||||
minimo = self._to_float(
|
||||
freq_min_map.get(parametro),
|
||||
default["min"],
|
||||
)
|
||||
|
||||
if minimo <= 0 and base > 0:
|
||||
minimo = base * 0.5
|
||||
|
||||
if base <= 0:
|
||||
base = default["base"]
|
||||
|
||||
if minimo > base:
|
||||
minimo = base * 0.5
|
||||
|
||||
em_uso = self._to_bool(
|
||||
freq_uso_map.get(parametro),
|
||||
default["em_uso"],
|
||||
)
|
||||
|
||||
return {
|
||||
"parametro": parametro,
|
||||
"freq": freq_real,
|
||||
"base": base,
|
||||
"min": minimo,
|
||||
"peso": default["peso"],
|
||||
"em_uso": em_uso,
|
||||
"existe_no_redis": parametro in freqs,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _score_frequencia(freq, base, minimo):
|
||||
if base <= 0:
|
||||
return 100.0
|
||||
|
||||
if freq >= base:
|
||||
return 100.0
|
||||
|
||||
if minimo <= 0:
|
||||
minimo = base * 0.5
|
||||
|
||||
if freq <= 0:
|
||||
return 0.0
|
||||
|
||||
if freq <= minimo:
|
||||
return max(0.0, min(50.0, 50.0 * (freq / minimo)))
|
||||
|
||||
faixa = base - minimo
|
||||
if faixa <= 0:
|
||||
return 50.0
|
||||
|
||||
return max(50.0, min(100.0, 50.0 + 50.0 * ((freq - minimo) / faixa)))
|
||||
|
||||
def _avaliar_frequencias(self, modulo):
|
||||
motivos = []
|
||||
detalhes = []
|
||||
soma = 0.0
|
||||
peso_total = 0.0
|
||||
algum_parametro_avaliado = False
|
||||
algum_freq_viva = False
|
||||
|
||||
# Avalia união entre defaults e frequências que vierem do Redis.
|
||||
freqs = self._as_dict(modulo.get("freq"))
|
||||
parametros = list(self.freq_defaults.keys())
|
||||
for p in freqs.keys():
|
||||
if p not in parametros:
|
||||
parametros.append(p)
|
||||
|
||||
for parametro in parametros:
|
||||
cfg = self._ler_freq_config(modulo, parametro)
|
||||
|
||||
# Se o C# disse explicitamente que não está em uso, ignora.
|
||||
if not cfg["em_uso"]:
|
||||
continue
|
||||
|
||||
# Se não existe no Redis e não é um default crítico, ignora para não criar alerta falso.
|
||||
if not cfg["existe_no_redis"] and parametro not in self.freq_defaults:
|
||||
continue
|
||||
|
||||
algum_parametro_avaliado = True
|
||||
|
||||
freq = cfg["freq"]
|
||||
base = cfg["base"]
|
||||
minimo = cfg["min"]
|
||||
peso = cfg["peso"]
|
||||
|
||||
if freq > 0:
|
||||
algum_freq_viva = True
|
||||
|
||||
score = self._score_frequencia(freq, base, minimo)
|
||||
|
||||
detalhes.append({
|
||||
"parametro": parametro,
|
||||
"freq": round(freq, 3),
|
||||
"freq_base": round(base, 3),
|
||||
"freq_min": round(minimo, 3),
|
||||
"score": round(score, 1),
|
||||
"em_uso": cfg["em_uso"],
|
||||
})
|
||||
|
||||
soma += score * peso
|
||||
peso_total += peso
|
||||
|
||||
if freq <= 0:
|
||||
motivos.append(
|
||||
f"Frequencia {parametro} ausente/zerada ({freq:.2f}/{base:.2f} Hz)."
|
||||
)
|
||||
elif freq < minimo:
|
||||
motivos.append(
|
||||
f"Frequencia {parametro} baixa ({freq:.2f}/{base:.2f} Hz, mínimo {minimo:.2f} Hz)."
|
||||
)
|
||||
|
||||
if not algum_parametro_avaliado:
|
||||
return 0.0, ["Nenhuma frequência de telemetria MOV disponível."], detalhes, False
|
||||
|
||||
score_final = soma / peso_total if peso_total > 0 else 0.0
|
||||
return score_final, motivos, detalhes, algum_freq_viva
|
||||
|
||||
def _adicionar_padrao_se_valido(self, padrao, valor):
|
||||
if valor is None:
|
||||
return
|
||||
|
||||
try:
|
||||
v = float(valor)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# Evita que zeros de boot contaminem o padrão dinâmico.
|
||||
if v == 0:
|
||||
return
|
||||
|
||||
padrao.adicionar_valor(v)
|
||||
|
||||
def _avaliar_valores_operacionais(self, endereco, modulo):
|
||||
condicoes = []
|
||||
motivos = []
|
||||
penalidade_total = 0.0
|
||||
|
||||
mod_id = modulo.get("mod_id")
|
||||
rpm = self._to_float(modulo.get("rpm"), 0.0)
|
||||
rpm_sp = self._to_float(modulo.get("rpm_sp"), 0.0)
|
||||
cod_erro = self._to_int(modulo.get("cod_erro"), 0)
|
||||
tensao = self._to_float(modulo.get("tensao"), 0.0)
|
||||
corrente_motor = self._to_float(modulo.get("corrente_motor"), 0.0)
|
||||
corrente_barramento = self._to_float(modulo.get("corrente_barramento"), 0.0)
|
||||
temperatura_motor = self._to_float(modulo.get("temperatura_motor"), 0.0)
|
||||
temperatura_driver = self._to_float(modulo.get("temperatura_driver"), 0.0)
|
||||
freado = self._to_bool(modulo.get("freado"), False)
|
||||
|
||||
padroes = self.padroes.get(endereco)
|
||||
if not isinstance(padroes, ModuloMovimentacaoPadroes):
|
||||
padroes = ModuloMovimentacaoPadroes()
|
||||
self.padroes[endereco] = padroes
|
||||
|
||||
self._adicionar_padrao_se_valido(padroes.padrao_tensao, tensao)
|
||||
self._adicionar_padrao_se_valido(padroes.padrao_corrente_motor, corrente_motor)
|
||||
self._adicionar_padrao_se_valido(padroes.padrao_corrente_barramento, corrente_barramento)
|
||||
self._adicionar_padrao_se_valido(padroes.padrao_temperatura_driver, temperatura_driver)
|
||||
self._adicionar_padrao_se_valido(padroes.padrao_temperatura_motor, temperatura_motor)
|
||||
|
||||
penalidade_erro = self.oid_erros_penalidades.get(cod_erro, 0)
|
||||
if penalidade_erro > 0:
|
||||
penalidade_total += min(100.0, penalidade_erro)
|
||||
desc = f"Erro ativo no OID: código {cod_erro}."
|
||||
motivos.append(desc)
|
||||
condicoes.append({
|
||||
"label": "Erro OID",
|
||||
"valor": cod_erro,
|
||||
"severidade": int(penalidade_erro),
|
||||
"descricao": desc,
|
||||
"acoes": [
|
||||
"Verificar código de falha do driver OID.",
|
||||
"Reduzir comando de movimento se o erro persistir.",
|
||||
"Inspecionar alimentação, cabos de fase e comunicação CAN."
|
||||
]
|
||||
})
|
||||
|
||||
# Erro de RPM só pesa quando existe SP significativo.
|
||||
limite_sp_rpm = 30.0
|
||||
limite_erro_pct = 0.30
|
||||
|
||||
if abs(rpm_sp) > limite_sp_rpm:
|
||||
erro_percentual_rpm = abs(rpm - rpm_sp) / max(abs(rpm_sp), 1.0)
|
||||
if erro_percentual_rpm > limite_erro_pct:
|
||||
penalidade = min(35, int(erro_percentual_rpm * 100))
|
||||
penalidade_total += penalidade
|
||||
desc = (
|
||||
f"RPM divergente (SP: {rpm_sp:.0f}, atual: {rpm:.0f}, "
|
||||
f"erro: {erro_percentual_rpm * 100:.1f}%)."
|
||||
)
|
||||
motivos.append(desc)
|
||||
condicoes.append({
|
||||
"label": "Acompanhamento de RPM",
|
||||
"valor": rpm,
|
||||
"severidade": penalidade,
|
||||
"descricao": desc,
|
||||
"acoes": [
|
||||
"Verificar se há roda travando, baixa aderência ou limitação de corrente.",
|
||||
"Conferir comando enviado ao OID e leitura de RPM.",
|
||||
"Reduzir velocidade de operação se houver oscilação persistente."
|
||||
]
|
||||
})
|
||||
|
||||
if freado and abs(rpm) > 30:
|
||||
penalidade = min(35, int(abs(rpm) / 5))
|
||||
penalidade_total += penalidade
|
||||
desc = f"Motor aparenta movimento com freio ativo (RPM atual: {rpm:.0f})."
|
||||
motivos.append(desc)
|
||||
condicoes.append({
|
||||
"label": "Freio / movimento",
|
||||
"valor": rpm,
|
||||
"severidade": penalidade,
|
||||
"descricao": desc,
|
||||
"acoes": [
|
||||
"Verificar estado real do freio.",
|
||||
"Verificar comando de parada e retorno do driver.",
|
||||
"Inspecionar encoder/RPM se a roda estiver parada fisicamente."
|
||||
]
|
||||
})
|
||||
|
||||
# Limites absolutos simples, sem depender só do padrão dinâmico.
|
||||
if tensao > 0 and (tensao < 28 or tensao > 45):
|
||||
penalidade = 30
|
||||
penalidade_total += penalidade
|
||||
desc = f"Tensão MOV fora da faixa esperada: {tensao:.1f} V."
|
||||
motivos.append(desc)
|
||||
condicoes.append({
|
||||
"label": "Tensão do driver",
|
||||
"valor": tensao,
|
||||
"severidade": penalidade,
|
||||
"descricao": desc,
|
||||
"acoes": ["Verificar alimentação dos drivers de movimento."]
|
||||
})
|
||||
|
||||
if abs(corrente_motor) > 45:
|
||||
penalidade = min(50, int(abs(corrente_motor) - 30))
|
||||
penalidade_total += penalidade
|
||||
desc = f"Corrente do motor elevada: {corrente_motor:.1f} A."
|
||||
motivos.append(desc)
|
||||
condicoes.append({
|
||||
"label": "Corrente do motor",
|
||||
"valor": corrente_motor,
|
||||
"severidade": penalidade,
|
||||
"descricao": desc,
|
||||
"acoes": [
|
||||
"Verificar esforço mecânico, roda travada ou terreno pesado.",
|
||||
"Reduzir aceleração/velocidade se o pico persistir."
|
||||
]
|
||||
})
|
||||
|
||||
if temperatura_driver > 70:
|
||||
penalidade = min(60, int(temperatura_driver - 50))
|
||||
penalidade_total += penalidade
|
||||
desc = f"Temperatura do driver elevada: {temperatura_driver:.1f} °C."
|
||||
motivos.append(desc)
|
||||
condicoes.append({
|
||||
"label": "Temperatura do driver",
|
||||
"valor": temperatura_driver,
|
||||
"severidade": penalidade,
|
||||
"descricao": desc,
|
||||
"acoes": [
|
||||
"Reduzir carga dos motores.",
|
||||
"Verificar ventilação do compartimento dos drivers."
|
||||
]
|
||||
})
|
||||
|
||||
# Padrões dinâmicos entram como alerta leve/moderado, para não assustar amanhã.
|
||||
checks_padrao = [
|
||||
("tensão fora do padrão", tensao, padroes.padrao_tensao),
|
||||
("corrente do motor fora do padrão", corrente_motor, padroes.padrao_corrente_motor),
|
||||
("corrente no barramento fora do padrão", corrente_barramento, padroes.padrao_corrente_barramento),
|
||||
("temperatura do driver fora do padrão", temperatura_driver, padroes.padrao_temperatura_driver),
|
||||
]
|
||||
|
||||
for label, valor, padrao in checks_padrao:
|
||||
if valor == 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
if not padrao.esta_dentro_do_padrao(valor):
|
||||
grau = min(25, int(padrao.calcular_quao_anormal(valor)))
|
||||
if grau > 0:
|
||||
penalidade_total += grau * 0.3
|
||||
condicoes.append({
|
||||
"label": "Padrão dinâmico MOV",
|
||||
"valor": valor,
|
||||
"severidade": grau,
|
||||
"descricao": f"{label}: {valor:.1f}",
|
||||
"acoes": [
|
||||
"Acompanhar se o desvio persiste.",
|
||||
"Comparar com outros drivers e com o esforço mecânico no momento."
|
||||
]
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
valor_score = max(0.0, 100.0 - penalidade_total)
|
||||
|
||||
detalhes = {
|
||||
"mod_id": mod_id,
|
||||
"rpm": rpm,
|
||||
"rpm_sp": rpm_sp,
|
||||
"cod_erro": cod_erro,
|
||||
"tensao": tensao,
|
||||
"corrente_motor": corrente_motor,
|
||||
"corrente_barramento": corrente_barramento,
|
||||
"temperatura_motor": temperatura_motor,
|
||||
"temperatura_driver": temperatura_driver,
|
||||
"freado": freado,
|
||||
"score_valores": round(valor_score, 1),
|
||||
"penalidade_valores": round(penalidade_total, 1),
|
||||
}
|
||||
|
||||
return valor_score, motivos, condicoes, detalhes
|
||||
|
||||
def atualizar_saude(self):
|
||||
try:
|
||||
saude_total = 0
|
||||
SAUDE_MIN_ALERTA = 80
|
||||
|
||||
dados_mov = ContextoGlobalRedis.get_modulo(self.t_code) or {}
|
||||
|
||||
saude_total = 0.0
|
||||
total_drivers_em_uso = 0
|
||||
motivos_dos_ativos = []
|
||||
saude_individual = []
|
||||
|
||||
SAUDE_MIN_ALERTA = 80
|
||||
LIMITE_ERRO_RPM = 30
|
||||
mod_conectado = False
|
||||
|
||||
dados_mov = ContextoGlobalRedis.get_modulo(self.t_code)
|
||||
ativos = self._listar_ativos(dados_mov)
|
||||
|
||||
ativos = dados_mov.get("ativos", [])
|
||||
for endereco in ativos:
|
||||
endereco = str(endereco) # Redis costuma serializar chave como string
|
||||
endereco = str(endereco)
|
||||
modulo = dados_mov.get(endereco)
|
||||
if not modulo:
|
||||
continue # ativo listado, mas sem dados (race condition)
|
||||
#for endereco_str, modulo in dados_mov.items():
|
||||
# try:
|
||||
# endereco = int(endereco_str)
|
||||
# except ValueError:
|
||||
# continue
|
||||
mod_id = modulo.get("mod_id")
|
||||
FREQ_BASE = modulo.get("freq_base", 1)
|
||||
FREQ_MIN = FREQ_BASE * 0.5
|
||||
saude = 100
|
||||
|
||||
if not isinstance(modulo, dict):
|
||||
continue
|
||||
|
||||
mod_id = modulo.get("mod_id", endereco)
|
||||
conectado = self._to_bool(modulo.get("conectado"), False)
|
||||
comandar = self._to_bool(modulo.get("comandar"), True)
|
||||
|
||||
motivos = []
|
||||
condicoes_operacionais = []
|
||||
conectado = modulo.get("conectado", False)
|
||||
comandar = modulo.get("comandar", True)
|
||||
freqs = modulo.get("freq", {})
|
||||
maior_freq = max(freqs.values() if freqs != {} else [0])
|
||||
detalhes = {}
|
||||
|
||||
if not conectado:
|
||||
saude = 0
|
||||
saude = 0.0
|
||||
motivos.append("Desconectado")
|
||||
elif maior_freq == 0:
|
||||
saude = 0
|
||||
motivos.append("Sem resposta de todos os parametros (freq = 0 Hz)")
|
||||
else:
|
||||
for label, freq in freqs.items():
|
||||
pen = penalizar_frequencia(freq, FREQ_BASE, FREQ_MIN, 5)
|
||||
saude -= pen
|
||||
if (freq <= FREQ_MIN):
|
||||
motivos.append(f"Frequencia {label} baixa ({freq:.2f}/{FREQ_BASE} Hz, penalidade {pen}%)")
|
||||
# Verificações de contexto
|
||||
rpm = modulo.get("rpm", 0)
|
||||
rpm_sp = modulo.get("rpm_sp", 0)
|
||||
cod_erro = modulo.get("cod_erro", 0)
|
||||
tensao = modulo.get("tensao", 0)
|
||||
corrente_motor = modulo.get("corrente_motor", 0)
|
||||
corrente_barramento = modulo.get("corrente_barramento", 0)
|
||||
temperatura_motor = modulo.get("temperatura_motor", 0)
|
||||
temperatura_driver = modulo.get("temperatura_driver", 0)
|
||||
freado = modulo.get("freado", False)
|
||||
idade = self._idade_modulo_s(modulo)
|
||||
if idade is not None:
|
||||
detalhes["idade_telemetria_s"] = round(idade, 3)
|
||||
|
||||
padroes = self.padroes.get(endereco)
|
||||
if not isinstance(padroes, ModuloMovimentacaoPadroes):
|
||||
padroes = ModuloMovimentacaoPadroes()
|
||||
self.padroes[endereco] = padroes
|
||||
padroes.padrao_tensao.adicionar_valor(tensao)
|
||||
padroes.padrao_corrente_motor.adicionar_valor(corrente_motor)
|
||||
padroes.padrao_corrente_barramento.adicionar_valor(corrente_barramento)
|
||||
padroes.padrao_temperatura_driver.adicionar_valor(temperatura_driver)
|
||||
score_freq, motivos_freq, detalhes_freq, alguma_freq_viva = \
|
||||
self._avaliar_frequencias(modulo)
|
||||
|
||||
OID_ERROS_PENALIDADES = {
|
||||
0: 0,
|
||||
1: 100,
|
||||
2: 80,
|
||||
3: 100,
|
||||
4: 80,
|
||||
5: 60,
|
||||
6: 100,
|
||||
7: 60,
|
||||
8: 100,
|
||||
9: 30,
|
||||
10: 30,
|
||||
11: 30,
|
||||
12: 70,
|
||||
13: 100,
|
||||
14: 100,
|
||||
15: 10,
|
||||
16: 0,
|
||||
17: 0,
|
||||
18: 40,
|
||||
19: 20
|
||||
}
|
||||
penalidade_erro = OID_ERROS_PENALIDADES.get(cod_erro, 0)
|
||||
if penalidade_erro > 0:
|
||||
condicoes_operacionais.append({
|
||||
"valor": cod_erro,
|
||||
"severidade": penalidade_erro,
|
||||
"descricao": f"erro ativo: código {cod_erro} (penalidade {penalidade_erro}%)"
|
||||
})
|
||||
valor_score, motivos_valores, cond_valores, detalhes_valores = \
|
||||
self._avaliar_valores_operacionais(endereco, modulo)
|
||||
|
||||
erro_percentual_rpm = abs(rpm - rpm_sp) / max(abs(rpm_sp), 1)
|
||||
if abs(rpm_sp) > LIMITE_ERRO_RPM and erro_percentual_rpm > (LIMITE_ERRO_RPM / 100.0):
|
||||
penalidade = min(int(erro_percentual_rpm * 100), LIMITE_ERRO_RPM)
|
||||
condicoes_operacionais.append({
|
||||
"valor": rpm,
|
||||
"severidade": penalidade,
|
||||
"descricao": f"RPM divergente (SP: {rpm_sp:.0f}, atual: {rpm:.0f}, erro: {erro_percentual_rpm*100:.1f}%, penalidade {penalidade}%)"
|
||||
})
|
||||
motivos.extend(motivos_freq)
|
||||
motivos.extend(motivos_valores)
|
||||
condicoes_operacionais.extend(cond_valores)
|
||||
|
||||
if freado and abs(rpm_sp) == 0 and erro_percentual_rpm > (LIMITE_ERRO_RPM / 100.0):
|
||||
penalidade = min(int(erro_percentual_rpm * 100), LIMITE_ERRO_RPM)
|
||||
condicoes_operacionais.append({
|
||||
"valor": 1,
|
||||
"severidade": penalidade,
|
||||
"descricao": f"motor em movimento mesmo com freio (RPM SP: {rpm_sp}, atual: {rpm}, erro {erro_percentual_rpm*100:.1f}%, penalidade {penalidade}%)"
|
||||
})
|
||||
|
||||
# 🔋 Tensão fora do padrão
|
||||
if not padroes.padrao_tensao.esta_dentro_do_padrao(tensao):
|
||||
grau = padroes.padrao_tensao.calcular_quao_anormal(tensao)
|
||||
condicoes_operacionais.append({
|
||||
"valor": tensao,
|
||||
"severidade": grau,
|
||||
"descricao": f"tensão fora do padrão: {tensao:.1f} V"
|
||||
})
|
||||
detalhes["frequencias"] = detalhes_freq
|
||||
detalhes.update(detalhes_valores)
|
||||
|
||||
# ⚡ Corrente do motor elevada
|
||||
if not padroes.padrao_corrente_motor.esta_dentro_do_padrao(corrente_motor):
|
||||
grau = padroes.padrao_corrente_motor.calcular_quao_anormal(corrente_motor)
|
||||
condicoes_operacionais.append({
|
||||
"valor": corrente_motor,
|
||||
"severidade": grau,
|
||||
"descricao": f"corrente do motor fora do padrão: {corrente_motor:.1f} A"
|
||||
})
|
||||
if not alguma_freq_viva:
|
||||
saude = 0.0
|
||||
motivos.append("Sem resposta dos parâmetros de movimentação (freq = 0 Hz).")
|
||||
else:
|
||||
# Comunicação/frequência é o principal para health.
|
||||
# Valores operacionais entram forte, mas sem transformar
|
||||
# uma frequência levemente baixa em falha falsa.
|
||||
saude = (0.65 * score_freq) + (0.35 * valor_score)
|
||||
|
||||
# ⚡ Corrente do barramento elevada
|
||||
if not padroes.padrao_corrente_barramento.esta_dentro_do_padrao(corrente_barramento):
|
||||
grau = padroes.padrao_corrente_barramento.calcular_quao_anormal(corrente_barramento)
|
||||
condicoes_operacionais.append({
|
||||
"valor": corrente_barramento,
|
||||
"severidade": grau,
|
||||
"descricao": f"corrente no barramento fora do padrão: {corrente_barramento:.1f} A"
|
||||
})
|
||||
if idade is not None and idade > self.timeout:
|
||||
penalidade_idade = min(40.0, (idade - self.timeout) * 8.0)
|
||||
saude -= penalidade_idade
|
||||
motivos.append(
|
||||
f"Telemetria MOV desatualizada ({idade:.1f}s sem atualização)."
|
||||
)
|
||||
condicoes_operacionais.append({
|
||||
"label": "Telemetria MOV",
|
||||
"valor": round(idade, 2),
|
||||
"severidade": int(penalidade_idade),
|
||||
"descricao": "Dados de movimentação desatualizados no Redis.",
|
||||
"acoes": [
|
||||
"Verificar rotina C# que publica dados MOV no Redis.",
|
||||
"Verificar comunicação CAN dos drivers OID.",
|
||||
"Conferir carga do Redis/health worker."
|
||||
]
|
||||
})
|
||||
|
||||
saude = max(0.0, min(100.0, round(saude, 1)))
|
||||
|
||||
status_individual = (
|
||||
StatusModulo.DESCONECTADO
|
||||
if not conectado
|
||||
else self._status_por_score(saude)
|
||||
)
|
||||
|
||||
# 🌡️ Temperatura alta (motor ou driver)
|
||||
if not padroes.padrao_temperatura_driver.esta_dentro_do_padrao(temperatura_driver):
|
||||
grau = padroes.padrao_temperatura_driver.calcular_quao_anormal(temperatura_driver)
|
||||
condicoes_operacionais.append({
|
||||
"valor": temperatura_driver,
|
||||
"severidade": grau,
|
||||
"descricao": f"temperatura elevada: motor {temperatura_motor:.1f}°C, driver {temperatura_driver:.1f}°C"
|
||||
})
|
||||
saude = max(saude, 0)
|
||||
saude_mod = {
|
||||
"id": endereco,
|
||||
"label": mod_id,
|
||||
"status": (StatusModulo.DESCONECTADO if not conectado else StatusModulo.OPERANTE if saude >= SAUDE_MIN_ALERTA else StatusModulo.ALERTA if saude > 0 else StatusModulo.FALHA).value,
|
||||
"saude": saude,
|
||||
"id": endereco,
|
||||
"label": mod_id,
|
||||
"status": status_individual.value,
|
||||
"saude": saude,
|
||||
"motivos": motivos,
|
||||
"condicoes_operacionais": condicoes_operacionais,
|
||||
"em_uso": comandar
|
||||
"em_uso": comandar,
|
||||
"detalhes": detalhes,
|
||||
}
|
||||
|
||||
saude_individual.append(saude_mod)
|
||||
|
||||
if comandar:
|
||||
saude_total += saude
|
||||
total_drivers_em_uso += 1
|
||||
motivos_dos_ativos.extend([f"Driver {endereco}: {m}" for m in motivos])
|
||||
motivos_dos_ativos.extend(
|
||||
[f"Driver {endereco}: {m}" for m in motivos]
|
||||
)
|
||||
|
||||
modulo["saude"] = saude_mod
|
||||
|
||||
if conectado:
|
||||
mod_conectado = True
|
||||
|
||||
saude_individual.sort(key=lambda x: x["id"])
|
||||
saude_individual.sort(key=lambda x: str(x["id"]))
|
||||
|
||||
saude_final = (saude_total // total_drivers_em_uso) if total_drivers_em_uso > 0 else 0
|
||||
if total_drivers_em_uso > 0:
|
||||
saude_final = round(saude_total / total_drivers_em_uso, 1)
|
||||
else:
|
||||
saude_final = 0.0
|
||||
|
||||
status = StatusModulo.OPERANTE
|
||||
|
||||
if not mod_conectado:
|
||||
status = StatusModulo.DESCONECTADO
|
||||
elif saude_final <= 0:
|
||||
status = StatusModulo.FALHA
|
||||
elif saude_final < SAUDE_MIN_ALERTA or any(saude["saude"] < SAUDE_MIN_ALERTA for saude in saude_individual):
|
||||
elif (
|
||||
saude_final < SAUDE_MIN_ALERTA or
|
||||
any(item["saude"] < SAUDE_MIN_ALERTA and item.get("em_uso", True)
|
||||
for item in saude_individual)
|
||||
):
|
||||
status = StatusModulo.ALERTA
|
||||
|
||||
saude_geral = {
|
||||
"conectado": mod_conectado,
|
||||
"status": status.value,
|
||||
"saude": saude_final,
|
||||
"motivos": motivos_dos_ativos,
|
||||
"saude_individual": saude_individual
|
||||
"status": status.value,
|
||||
"saude": saude_final,
|
||||
"motivos": motivos_dos_ativos,
|
||||
"saude_individual": saude_individual,
|
||||
"detalhes": {
|
||||
"drivers_ativos": ativos,
|
||||
"drivers_em_uso": total_drivers_em_uso,
|
||||
"modelo_frequencia": "por_parametro",
|
||||
}
|
||||
}
|
||||
|
||||
#print(saude_geral)
|
||||
|
||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||
ContextoGlobalRedis.ModKey(self.t_code),
|
||||
ContextoGlobalRedis.ModKey(self.t_code),
|
||||
saude=saude_geral
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}")
|
||||
|
|
|
|||
Loading…
Reference in New Issue