diff --git a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs index 215821eef..e7e7d2f57 100644 --- a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs +++ b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs @@ -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) { diff --git a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs index 2b0b0684a..99266eac5 100644 --- a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs +++ b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs @@ -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) { diff --git a/AgroBase/AgroBase/Services/OIDCanService.cs b/AgroBase/AgroBase/Services/OIDCanService.cs index 6ef538e18..1968782b5 100644 --- a/AgroBase/AgroBase/Services/OIDCanService.cs +++ b/AgroBase/AgroBase/Services/OIDCanService.cs @@ -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]; diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/bateria.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/bateria.py index 677042ef7..57995af47 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/bateria.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/bateria.py @@ -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), diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/movimentacao.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/movimentacao.py index 4cacda177..d068402d5 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/movimentacao.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/movimentacao.py @@ -1,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}")