ajustes finais timeout bateria e movimentacao

This commit is contained in:
Diego Freitas 2026-06-23 07:46:11 -03:00
parent c306c8727b
commit 724391826b
5 changed files with 731 additions and 192 deletions

View File

@ -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)
{

View File

@ -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)
{

View File

@ -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];

View File

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

View File

@ -1,101 +1,113 @@
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 = {}
def atualizar_saude(self):
try:
saude_total = 0
total_drivers_em_uso = 0
motivos_dos_ativos = []
saude_individual = []
# 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,
},
}
SAUDE_MIN_ALERTA = 80
LIMITE_ERRO_RPM = 30
mod_conectado = False
dados_mov = ContextoGlobalRedis.get_modulo(self.t_code)
ativos = dados_mov.get("ativos", [])
for endereco in ativos:
endereco = str(endereco) # Redis costuma serializar chave como string
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
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])
if not conectado:
saude = 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)
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)
OID_ERROS_PENALIDADES = {
self.oid_erros_penalidades = {
0: 0,
1: 100,
2: 80,
3: 100,
4: 80,
5: 60,
6: 100,
7: 60,
8: 100,
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,
@ -106,97 +118,577 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
16: 0,
17: 0,
18: 40,
19: 20
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}%)"
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"],
})
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({
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": f"RPM divergente (SP: {rpm_sp:.0f}, atual: {rpm:.0f}, erro: {erro_percentual_rpm*100:.1f}%, penalidade {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_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,
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": f"motor em movimento mesmo com freio (RPM SP: {rpm_sp}, atual: {rpm}, erro {erro_percentual_rpm*100:.1f}%, penalidade {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."
]
})
# 🔋 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({
# 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": grau,
"descricao": f"tensão fora do padrão: {tensao:.1f} V"
"severidade": penalidade,
"descricao": desc,
"acoes": ["Verificar alimentação dos drivers de movimento."]
})
# ⚡ 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({
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": grau,
"descricao": f"corrente do motor fora do padrão: {corrente_motor:.1f} A"
"severidade": penalidade,
"descricao": desc,
"acoes": [
"Verificar esforço mecânico, roda travada ou terreno pesado.",
"Reduzir aceleração/velocidade se o pico persistir."
]
})
# ⚡ 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"
})
# 🌡️ 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({
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": grau,
"descricao": f"temperatura elevada: motor {temperatura_motor:.1f}°C, driver {temperatura_driver:.1f}°C"
"severidade": penalidade,
"descricao": desc,
"acoes": [
"Reduzir carga dos motores.",
"Verificar ventilação do compartimento dos drivers."
]
})
saude = max(saude, 0)
# 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_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 = []
mod_conectado = False
ativos = self._listar_ativos(dados_mov)
for endereco in ativos:
endereco = str(endereco)
modulo = dados_mov.get(endereco)
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 = []
detalhes = {}
if not conectado:
saude = 0.0
motivos.append("Desconectado")
else:
idade = self._idade_modulo_s(modulo)
if idade is not None:
detalhes["idade_telemetria_s"] = round(idade, 3)
score_freq, motivos_freq, detalhes_freq, alguma_freq_viva = \
self._avaliar_frequencias(modulo)
valor_score, motivos_valores, cond_valores, detalhes_valores = \
self._avaliar_valores_operacionais(endereco, modulo)
motivos.extend(motivos_freq)
motivos.extend(motivos_valores)
condicoes_operacionais.extend(cond_valores)
detalhes["frequencias"] = detalhes_freq
detalhes.update(detalhes_valores)
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)
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)
)
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,
"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 = {
@ -204,14 +696,18 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
"status": status.value,
"saude": saude_final,
"motivos": motivos_dos_ativos,
"saude_individual": saude_individual
"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),
saude=saude_geral
)
except Exception as e:
print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}")