robustez nas métricas de saúde dos módulos Mov, Dir, Sen e Atu

This commit is contained in:
Diego Freitas 2026-06-24 13:32:47 -03:00
parent 2d6300d621
commit 97c9a49f3e
15 changed files with 5078 additions and 2011 deletions

View File

@ -475,7 +475,7 @@ namespace AgroBase.Forms.IHM
await EnviarComandoBico(B01, true, 3000, Testes[idx].ValorPadrao, Dispositivo.Dados);
double Fluxo = FluxoInicializado ? Dispositivo.Dados.VazaoFluxoMLpSInstantanea : -1;
double Fluxo = FluxoInicializado ? Dispositivo.Dados.VazaoFluxoMLpSInstantaneaCorrigida : -1;
idx = PreencheDadosLinha(grid, Testes, idx, Fluxo);
bool statusB01_1 = B01Inicializado && B01._EstadoLeitura == Estado.Ligado;
@ -550,7 +550,7 @@ namespace AgroBase.Forms.IHM
{
await Task.Delay(100);
if (disp != null && disp.VazaoFluxoMLpSInstantanea >= ValorComparar)
if (disp != null && disp.VazaoFluxoMLpSInstantaneaCorrigida >= ValorComparar)
{
break;
}

View File

@ -32,13 +32,13 @@ namespace AgroBase.Forms
switch (comando.Dispositivo)
{
case T_Code.Mov:
GeneralJoystick.ProcessarDadosControle(comando.botaoJoy, false, key: comando.key, valorDesejado: Variaveis.OperacaoEmAndamento.Parametros.Controle.MovVelocidadeCErvasPercent);
GeneralJoystick.ProcessarDadosControle(comando.botaoJoy, false, key: comando.key, valorDesejado: Variaveis.OperacaoEmAndamento.Parametros.Controle.MovVelocidadeCErvasPercent, ForcarComando: true);
break;
case T_Code.Dir:
GeneralJoystick.ProcessarDadosControle(comando.botaoJoy, false, key: comando.key, valorDesejado: Variaveis.OperacaoEmAndamento.Parametros.Controle.DirAnguloMaximo);
GeneralJoystick.ProcessarDadosControle(comando.botaoJoy, false, key: comando.key, valorDesejado: Variaveis.OperacaoEmAndamento.Parametros.Controle.DirAnguloMaximo, ForcarComando: true);
break;
case T_Code.Atu:
GeneralJoystick.ProcessarDadosControle(comando.botaoJoy, false, key: comando.key);
GeneralJoystick.ProcessarDadosControle(comando.botaoJoy, false, key: comando.key, ForcarComando: true);
break;
}
}
@ -47,7 +47,7 @@ namespace AgroBase.Forms
{
var comando = GeneralJoystick.DeParaComandos.FirstOrDefault(x => x.key == e.KeyCode);
if (comando == null) return;
GeneralJoystick.ProcessarDadosControle(comando.botaoJoy, true, key: comando.key);
GeneralJoystick.ProcessarDadosControle(comando.botaoJoy, true, key: comando.key, ForcarComando: true);
}
}

View File

@ -1,15 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System;
using static AgroBase.Models.Enums;
using AgroBase.Forms;
using AgroBase.Forms;
using AgroBase.Models;
using System.Threading.Tasks;
using AgroBase.Models.Modules;
using SharpDX.DirectInput;
using AgroBase.Services;
using SharpDX.DirectInput;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using static AgroBase.Models.Enums;
namespace AgroBase
{
@ -927,6 +928,8 @@ namespace AgroBase
{
Sinaleiro.Comportamento = (SensorSinaleiroComportamentoModel)Status;
(id_num, posicao, data) = Sinaleiro.ProtocoloComando();
Sinaleiro.UltimoComandoEnviado = DateTime.Now;
Sinaleiro.UltimoComportamentoDesejado = (SensorSinaleiroComportamentoModel)Status;
}
break;
case S_Code.sRLE:
@ -935,6 +938,8 @@ namespace AgroBase
{
Rele.StatusControle = (Estado)Status;
(id_num, posicao, data) = Rele.ProtocoloComando();
Rele.UltimoComandoEnviado = DateTime.Now;
Rele.UltimoEstadoDesejado = (Estado)Status;
}
break;
case S_Code.sSRV:
@ -943,6 +948,8 @@ namespace AgroBase
{
ServoFreio._AnguloControle = (int)Status;
(id_num, posicao, data) = ServoFreio.ProtocoloComando();
ServoFreio.UltimoComandoEnviado = DateTime.Now;
ServoFreio.UltimoAnguloDesejado = (int)Status;
}
break;
}

View File

@ -121,10 +121,14 @@ namespace AgroBase.Models.Modules
[JsonIgnore]
private SensorModel SensorFluxoLinha => Sensores.FirstOrDefault(x => x.Componente == S_Code.sFLX && x.ID == "FLXLN");
const double FatorCalibracaoFluxo = 0.776;
public double VazaoMaximaSensor_LMin { get; set; } = 20.0;
public double VazaoFluxoMLpSInstantanea => SensorFluxoLinha?.ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.Fluxo)?.atual?.valor ?? 0;
public double VazaoFluxoMLpSInstantaneaCorrigida => VazaoFluxoMLpSInstantanea * FatorCalibracaoFluxo;
public double VazaoFluxoMLpSInstantaneaCalc
{
get
@ -140,8 +144,6 @@ namespace AgroBase.Models.Modules
if (pulsos <= 0 || periodo <= 0)
return 0.0;
const double FatorCalibracaoFluxo = 0.776;
double frequencia = (double)pulsos / periodo * 1000.0;
double q_L_min = (frequencia + 3.0) / 8.1;
@ -978,9 +980,9 @@ namespace AgroBase.Models.Modules
posicao = CanMessagePosicaoDados.Dados1,
indice = 1,
analise_health = true,
maximo = 35,
minimo = 0,
nominal = 30,
maximo = VariaveisEquipamento.CapacidadeReservatorio + 10.0,
minimo = VariaveisEquipamento.PercentualReservatorioMinCritio,
nominal = VariaveisEquipamento.CapacidadeReservatorio,
unidade_medida = "Kg"
}
}
@ -2049,6 +2051,9 @@ namespace AgroBase.Models.Modules
};
dispAtu.AdicionarMensagemFila(bico.ID_Num, CanMessagePosicaoDados.Command1, CanMessagePosicaoDados.Command1, data, false);
bico.UltimoComandoEnviado = DateTime.Now;
bico.UltimoStatusDesejado = status;
bico.UltimoAnguloDesejado = angulo;
}
private void EnviarComandoFisicoBomba(AtuadorBombaModel bomba, bool status, double potencia, double pressao)
@ -2074,6 +2079,11 @@ namespace AgroBase.Models.Modules
};
dispAtu.AdicionarMensagemFila(bomba.ID_Num, CanMessagePosicaoDados.Command1, CanMessagePosicaoDados.Command1, data, false);
bomba.UltimoComandoEnviado = DateTime.Now;
bomba.UltimoStatusDesejado = status;
bomba.UltimaPotenciaDesejada = potencia;
bomba.UltimaPressaoDesejada = pressao;
}
@ -2295,6 +2305,9 @@ namespace AgroBase.Models.Modules
public Estado _EstadoLeitura => (Estado)(ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.EstadoLeitura)?.atual?.valor ?? 0);
public double _AnguloBicoLeitura => ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.ServoAnguloLeitura)?.atual? .valor ?? 0;
public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue;
public bool UltimoStatusDesejado { get; set; } = false;
public double UltimoAnguloDesejado { get; set; } = 0;
public (CanMessagePosicaoDados, byte[]) ProtocoloConfiguracao(PinoutDataModel pinout, bool conectar)
{
@ -2529,6 +2542,11 @@ namespace AgroBase.Models.Modules
public double PotenciaMediaAtiva { get; set; }
public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue;
public bool UltimoStatusDesejado { get; set; } = false;
public double UltimaPotenciaDesejada { get; set; } = 0;
public double UltimaPressaoDesejada { get; set; } = 0;
public List<(CanMessagePosicaoDados, byte[])> ProtocoloConfiguracao(PinoutDataModel pinout, bool conectar)

View File

@ -266,6 +266,13 @@ namespace AgroBase.Models.Modules
public MvdServoFreioModel ConfigFreio { get; set; }
public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue;
public OidFuncCode UltimoModoControleEnviado { get; set; }
public double UltimoSetPointEnviado { get; set; } = 0;
public Direcao UltimaDirecao { get; set; } = Direcao.Parado;
public bool UltimoFreioSP { get; set; } = false;
public static MovimentacaoModel CarregarParametrosIniciais(string Mod_ID, byte _EnderecoTx, byte _EnderecoRx)
{
return new MovimentacaoModel()
@ -450,6 +457,11 @@ namespace AgroBase.Models.Modules
OIDCanService.ComandoControle(_EnderecoCAN_Tx, _EnderecoCAN_Rx, modoControle, setPoint);
LeituraRPM = RPM_SP;
UltimoComandoEnviado = DateTime.Now;
UltimoModoControleEnviado = modoControle;
UltimoSetPointEnviado = setPoint;
UltimaDirecao = DirecaoAtual;
UltimoFreioSP = Freado;
}
}
}

View File

@ -238,7 +238,6 @@ namespace AgroBase.Models.Modules
private DateTime _ultimoEnvioControleMov = DateTime.MinValue;
private DateTime _ultimoRegistroLogMov = DateTime.MinValue;
private static readonly TimeSpan IntervaloControleMov = TimeSpan.FromSeconds(1);
private static readonly TimeSpan IntervaloLogMov = TimeSpan.FromSeconds(1);
private async Task tmrRegistradorMOV_Tick()
@ -340,10 +339,8 @@ namespace AgroBase.Models.Modules
private DateTime _ultimoEnvioControleDir = DateTime.MinValue;
private DateTime _ultimoSensoresExternosDir = DateTime.MinValue;
private static readonly TimeSpan IntervaloControleDir = TimeSpan.FromMilliseconds(250);
private static readonly TimeSpan IntervaloSensoresExternosDir = TimeSpan.FromMilliseconds(500);
private async Task tmrRegistradorDIR_Tick()
{
if (Variaveis.Fechando)
@ -351,11 +348,9 @@ namespace AgroBase.Models.Modules
DateTime agora = DateTime.Now;
if (!MKS057DCanService.Iniciado &&
SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Dir))
if (!MKS057DCanService.Iniciado && SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Dir))
{
foreach (var leitura in MKS057DCanService.DadosLeitura
.Where(x => x.UltimoComandoRecebido.AddSeconds(10) < agora))
foreach (var leitura in MKS057DCanService.DadosLeitura.Where(x => x.UltimoComandoRecebido.AddSeconds(10) < agora))
{
/*
* Poke leve para tentar recuperar leitura.

View File

@ -1818,6 +1818,7 @@ namespace AgroBase.Models.Modules
},
AnguloNeutro = 0,
_AnguloControle = 0,
UltimoAnguloDesejado = 0,
Incremental = true,
ValoresLeituras = new List<SensorValoresLeituraModel>()
{
@ -1850,6 +1851,7 @@ namespace AgroBase.Models.Modules
},
AnguloNeutro = 0,
_AnguloControle = 0,
UltimoAnguloDesejado = 0,
Incremental = true,
ValoresLeituras = new List<SensorValoresLeituraModel>()
{
@ -1882,6 +1884,7 @@ namespace AgroBase.Models.Modules
},
AnguloNeutro = 180,
_AnguloControle = 180,
UltimoAnguloDesejado = 180,
Incremental = false,
ValoresLeituras = new List<SensorValoresLeituraModel>()
{
@ -1914,6 +1917,7 @@ namespace AgroBase.Models.Modules
},
AnguloNeutro = 180,
_AnguloControle = 180,
UltimoAnguloDesejado = 180,
Incremental = false,
ValoresLeituras = new List<SensorValoresLeituraModel>()
{
@ -2487,6 +2491,10 @@ namespace AgroBase.Models.Modules
public int _StatusLedId => (int)(ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.StatusLedID)?.atual?.valor ?? 99);
public StatusLED _StatusLed => (StatusLED)(ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.StatusLedLeitura)?.atual?.valor ?? StatusLED.Apagado);
public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue;
public SensorSinaleiroComportamentoModel UltimoComportamentoDesejado { get; set; }
public (CanMessagePosicaoDados, byte[]) ProtocoloConfiguracao(PinoutDataModel pinout, bool conectar)
{
var pino = pinout.Pinos.FirstOrDefault(p => p.ComponenteID == ID);
@ -2547,6 +2555,10 @@ namespace AgroBase.Models.Modules
public Estado _EstadoLeitura => ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.EstadoLeitura)?.atual?.valor ?? Estado.Desligado;
public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue;
public Estado UltimoEstadoDesejado { get; set; } = Estado.Desligado;
public (CanMessagePosicaoDados, byte[]) ProtocoloConfiguracao(PinoutDataModel pinout, bool conectar)
{
var pino = pinout.Pinos.FirstOrDefault(p => p.ComponenteID == ID);
@ -2607,6 +2619,9 @@ namespace AgroBase.Models.Modules
}
public List<SensorValoresLeituraModel> ValoresLeituras { get; set; }
public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue;
public double UltimoAnguloDesejado { get; set; } = 0;
public (CanMessagePosicaoDados, byte[]) ProtocoloConfiguracao(PinoutDataModel pinout, bool conectar)
{
var pino = pinout.Pinos.FirstOrDefault(p => p.ComponenteID == ID);

View File

@ -3038,7 +3038,7 @@ namespace AgroBase.Models
PressaoLinha = Variaveis.OperacaoEmAndamento.DispAtu.Dados.PressaoLinha,
PotenciaBomba = Variaveis.OperacaoEmAndamento.DispAtu.Dados.BombaPressurizadora?._Potencia ?? 0,
BombaLigada = Variaveis.OperacaoEmAndamento.DispAtu.Dados.BombaPressurizadora?.ComandoAtuar ?? false,
VazaoFluxoMLpSInstantanea = Variaveis.OperacaoEmAndamento.DispAtu.Dados.VazaoFluxoMLpSInstantanea,
VazaoFluxoMLpSInstantanea = Variaveis.OperacaoEmAndamento.DispAtu.Dados.VazaoFluxoMLpSInstantaneaCorrigida,
VazaoFluxoMLpSMedia = Variaveis.OperacaoEmAndamento.DispAtu.Dados.VazaoMediaMLs,
VolumeVazaoML = Variaveis.OperacaoEmAndamento.DispAtu.Dados.VolumeVazadoML,
QuantidadeBicos = Variaveis.OperacaoEmAndamento.Parametros.QtdBicos ?? VariaveisEquipamento.QuantidadeBicosPulverizadores,
@ -3665,19 +3665,26 @@ namespace AgroBase.Models
public void InserirLog(T_Code dispositivo, StatusModulo status, double saude, string mensagem, string id = null, string condicoes = null)
{
if (Logs == null)
Logs = new List<OperacaoSensoriamentoLogErrosModel>();
Logs.Add(new OperacaoSensoriamentoLogErrosModel()
try
{
Momento = DateTime.Now,
Dispositivo = dispositivo,
ID = id,
Status = status,
Saude = saude,
Mensagem = mensagem,
Condicoes = condicoes
});
if (Logs == null)
Logs = new List<OperacaoSensoriamentoLogErrosModel>();
Logs.Add(new OperacaoSensoriamentoLogErrosModel()
{
Momento = DateTime.Now,
Dispositivo = dispositivo,
ID = id,
Status = status,
Saude = saude,
Mensagem = mensagem,
Condicoes = condicoes
});
}
catch (Exception ex)
{
Variaveis.MostrarLog($"[OperacaoModel.InserirLog] Erro ao inserir log: {ex.Message}");
}
}
@ -3736,7 +3743,7 @@ namespace AgroBase.Models
MassaReservatorio = _Reservatorio.MassaAtual_Kg,
PressaoLinha = _DispAtu.Dados.PressaoLinha,
PressaoLinhaCalc = _DispAtu.Dados.PressaoLinhaCalc,
VazaoInstantanea = _DispAtu.Dados.VazaoFluxoMLpSInstantanea,
VazaoInstantanea = _DispAtu.Dados.VazaoFluxoMLpSInstantaneaCorrigida,
VazaoInstantaneaCalc = _DispAtu.Dados.VazaoFluxoMLpSInstantaneaCalc,
VazaoMedia = _DispAtu.Dados.VazaoMediaMLs,
TempoBombaAtuada = _DispAtu.Dados.TempoAtuado,

File diff suppressed because it is too large Load Diff

View File

@ -112,7 +112,7 @@ namespace AgroBase.Services
Parametro = OidParametros.VelocidadeAtual,
RedisKey = "rpm",
FreqBaseHz = 1.0,
FreqMinHz = 0.45,
FreqMinHz = 0.35,
EmUsoHealth = true
}
},
@ -123,7 +123,7 @@ namespace AgroBase.Services
Parametro = OidParametros.CorrenteMotor,
RedisKey = "corrente_motor",
FreqBaseHz = 1.0,
FreqMinHz = 0.45,
FreqMinHz = 0.35,
EmUsoHealth = true
}
},
@ -134,7 +134,7 @@ namespace AgroBase.Services
Parametro = OidParametros.CorrenteBarramento,
RedisKey = "corrente_barramento",
FreqBaseHz = 1.0,
FreqMinHz = 0.45,
FreqMinHz = 0.35,
EmUsoHealth = true
}
},

View File

@ -1,6 +1,7 @@
using AgroBase.Models;
using AgroBase.Models.Operadores;
using Newtonsoft.Json;
using OpenHardwareMonitor.Hardware;
using System;
using System.Collections.Generic;
using System.Linq;
@ -417,6 +418,7 @@ namespace AgroBase.Services.Operadores
if (Variaveis.OperacaoEmAndamento.DispSen == null) return;
var _Sensoriamento = Variaveis.OperacaoEmAndamento.Sensoriamento;
var agora_dt = DateTime.Now;
var agora = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
//Console.WriteLine(RedisService.Get(RedisService.ModKey(Enums.T_Code.Gps)));
@ -431,27 +433,90 @@ namespace AgroBase.Services.Operadores
var dirAtivos = new List<int>();
var movAtivos = new List<int>();
foreach (var mod in Variaveis.OperacaoEmAndamento.DispMvd.Dados.Modulos)
{
double erroAnguloDir = Math.Abs(mod.DirMotor.AnguloLeitura - mod.DirMotor.Angulo_SP);
double idadeUltimoComandoMs =
mod.DirMotor.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - mod.DirMotor.UltimoComandoEnviado).TotalMilliseconds;
double idadeUltimaRespostaMs =
mod.DirMotor.Leitura?.UltimoComandoRecebido == DateTime.MinValue
? 999999
: (agora_dt - (mod.DirMotor.Leitura?.UltimoComandoRecebido ?? DateTime.MinValue)).TotalMilliseconds;
int endereco_dir = mod.DirMotor._EnderecoCAN_Tx;
RedisService.AtualizarCampos(
RedisService.ModKey(Enums.T_Code.Dir),
var camposDir = new List<(string campo, object valor)>
{
($"{endereco_dir}.timestamp", agora),
($"{endereco_dir}.conectado", mod.DirMotor.Inicializado),
// legado
($"{endereco_dir}.freq_base", Variaveis.OperacaoEmAndamento.DispMvd.Dados.tmrRegistradorDIR.IntervalHz),
($"{endereco_dir}.endereco", mod.DirMotor._EnderecoCAN_Tx),
($"{endereco_dir}.mod_id", mod.Modulo_ID),
($"{endereco_dir}.angulo", mod.DirMotor.AnguloLeitura),
($"{endereco_dir}.angulo_sp", mod.DirMotor.Angulo_SP),
($"{endereco_dir}.comandar", mod.DirMotor.Comandar),
($"{endereco_dir}.referenciado", mod.DirMotor.Referenciado),
($"{endereco_dir}.in1_atuado", mod.DirMotor.IN1_Atuado),
($"{endereco_dir}.sentido", mod.DirMotor.Sentido),
($"{endereco_dir}.sentido_sp", mod.DirMotor.Sentido_SP),
($"{endereco_dir}.sentido_real", mod.DirMotor.SentidoReal),
($"{endereco_dir}.freq.pulsos", mod.DirMotor.Leitura?.NumeroPulsos?.frequencia ?? 0),
($"{endereco_dir}.freq.io", mod.DirMotor.Leitura?.EntradaIN1?.frequencia ?? 0)
);
($"{endereco_dir}.erro_angulo", erroAnguloDir),
($"{endereco_dir}.atingiu_angulo_sp", mod.DirMotor.AtingiuAnguloSP),
($"{endereco_dir}.ultimo_comando_ms", idadeUltimoComandoMs),
($"{endereco_dir}.ultima_resposta_ms", idadeUltimaRespostaMs),
($"{endereco_dir}.ultima_direcao", mod.DirMotor.UltimaDirecao),
($"{endereco_dir}.status_movimento", mod.DirMotor.Leitura?.StatusMovimento),
($"{endereco_dir}.rpm", mod.DirMotor.RPM),
($"{endereco_dir}.em_movimento", mod.DirMotor.EmMovimento),
($"{endereco_dir}.retornando_a_zero", mod.DirMotor.RetornandoAzero),
// frequências reais medidas
($"{endereco_dir}.freq.pulsosrecebidos", mod.DirMotor.Leitura?.NumeroPulsos?.frequencia ?? 0),
($"{endereco_dir}.freq.io", mod.DirMotor.Leitura?.EntradaIN1?.frequencia ?? 0),
($"{endereco_dir}.freq.rpm", mod.DirMotor.Leitura?.RPM?.frequencia ?? 0),
// mantém disponíveis para diagnóstico futuro, mesmo que não estejam no polling normal
($"{endereco_dir}.freq.erro", 0),
($"{endereco_dir}.freq.encoder_carregado", 0),
($"{endereco_dir}.freq.encoder_acumulado", 0),
($"{endereco_dir}.freq.versao", 0),
};
camposDir.AddRange(MKS057DCanService.GerarCamposRedisHealthBase(endereco_dir));
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Dir), camposDir.ToArray());
double erroRpmMov = Math.Abs(mod.MovMotor.RPM_Roda - mod.MovMotor.RPM_SP_Leitura);
double erroRpmPctMov =
Math.Abs(mod.MovMotor.RPM_SP_Leitura) > 1
? erroRpmMov / Math.Abs(mod.MovMotor.RPM_SP_Leitura)
: 0;
bool atingiuRpmSp =
Math.Abs(mod.MovMotor.RPM_SP_Leitura) < 25
? Math.Abs(mod.MovMotor.RPM_Roda) < 35
: erroRpmPctMov <= 0.45 || erroRpmMov <= 45;
double idadeUltimoComandoMovMs =
mod.MovMotor.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - mod.MovMotor.UltimoComandoEnviado).TotalMilliseconds;
double idadeUltimaRespostaMovMs =
mod.MovMotor.Leitura?.UltimoComandoRecebido == DateTime.MinValue
? 999999
: (agora_dt - (mod.MovMotor.Leitura?.UltimoComandoRecebido ?? DateTime.MinValue)).TotalMilliseconds;
int endereco_mov = mod.MovMotor._EnderecoCAN_Tx;
var camposMov = new List<(string campo, object valor)>
@ -460,10 +525,7 @@ namespace AgroBase.Services.Operadores
($"{endereco_mov}.conectado", mod.MovMotor.Inicializado),
// legado
($"{endereco_mov}.freq_base",
OIDCanService.ObterFreqBaseHz(
OIDCanService.OidHandler.OidParametros.VelocidadeAtual
)),
($"{endereco_mov}.freq_base", OIDCanService.ObterFreqBaseHz(OIDCanService.OidHandler.OidParametros.VelocidadeAtual)),
($"{endereco_mov}.endereco", mod.MovMotor._EnderecoCAN_Tx),
($"{endereco_mov}.mod_id", mod.Modulo_ID),
@ -487,26 +549,60 @@ namespace AgroBase.Services.Operadores
($"{endereco_mov}.freq.ciclo_trabalho", mod.MovMotor.Leitura?.CicloTrabalho?.frequencia ?? 0),
($"{endereco_mov}.freq.potencia", mod.MovMotor.Leitura?.Potencia?.frequencia ?? 0),
($"{endereco_mov}.comandar", mod.MovMotor.Comandar),
($"{endereco_mov}.comando_ligado", mod.MovMotor.ComandoLigado),
($"{endereco_mov}.leitura_ligado", mod.MovMotor.LeituraLigado),
($"{endereco_mov}.alarme_motor", mod.MovMotor.AlarmeMotor),
($"{endereco_mov}.alarme_comando", mod.MovMotor.AlarmeComando),
($"{endereco_mov}.alarme_sensores", mod.MovMotor.AlarmeSensores),
($"{endereco_mov}.sentido", mod.MovMotor.Sentido),
($"{endereco_mov}.sentido_sp", mod.MovMotor.Sentido_SP),
($"{endereco_mov}.erro_rpm", erroRpmMov),
($"{endereco_mov}.erro_rpm_pct", erroRpmPctMov),
($"{endereco_mov}.atingiu_rpm_sp", atingiuRpmSp),
($"{endereco_mov}.ultimo_comando_ms", idadeUltimoComandoMovMs),
($"{endereco_mov}.ultima_resposta_ms", idadeUltimaRespostaMovMs),
($"{endereco_mov}.freio_sp", mod.MovMotor.UltimoFreioSP),
($"{endereco_mov}.modo_controle", mod.MovMotor.Leitura?.ModoControle),
($"{endereco_mov}.ultimo_modo_controle", mod.MovMotor.UltimoModoControleEnviado),
($"{endereco_mov}.ultimo_setpoint", mod.MovMotor.UltimoSetPointEnviado),
};
camposMov.AddRange(OIDCanService.GerarCamposRedisHealthBase(endereco_mov));
RedisService.AtualizarCampos(
RedisService.ModKey(Enums.T_Code.Mov),
camposMov.ToArray()
);
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Mov), camposMov.ToArray());
dirAtivos.Add(endereco_dir);
movAtivos.Add(endereco_mov);
}
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Dir), ($"ativos", dirAtivos.ToList()));
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Mov), ($"ativos", movAtivos.ToList()));
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Dir), ("ativos", dirAtivos.ToList()));
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Mov), ("ativos", movAtivos.ToList()));
var DadosSen = Variaveis.OperacaoEmAndamento.DispSen.Dados;
//Console.WriteLine("[HEALTH SEN] " + DadosSen.Conectado);
DateTime ultimaRespostaSen =
DadosSen.DadosLeitura?.UltimoComandoRespondido ?? DateTime.MinValue;
double idadeUltimaRespostaSenMs =
ultimaRespostaSen == DateTime.MinValue
? 999999
: (agora_dt - ultimaRespostaSen).TotalMilliseconds;
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Sen),
("timestamp", agora),
("conectado", DadosSen.Conectado),
("freq_base", DadosSen.FrequenciaBase),
("bms_ligado", _Sensoriamento.Bateria?.BmsLigado ?? false)
("bms_ligado", _Sensoriamento.Bateria?.BmsLigado ?? false),
("ultima_resposta_ms", idadeUltimaRespostaSenMs),
("latencia_loop_ms", DadosSen.DadosLeitura?.LatenciaLoop ?? 0),
("percentual_bateria", _Sensoriamento.Bateria?.PercentualBateria ?? 0)
);
var senSensoresAtualizados = new List<(string caminho, object valor)>();
var senSensoresAtivos = new List<string>();
@ -527,10 +623,16 @@ namespace AgroBase.Services.Operadores
maximo = y.maximo,
minimo = y.minimo,
atual = y.atual.valor,
freq = y.atual.frequencia
freq = y.atual.frequencia,
unidade = y.unidade_medida,
})
.ToList();
double idadeUltimaRespostaSensorMs =
sensor.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - sensor.UltimaLeitura).TotalMilliseconds;
var sensorObj = new
{
conectado = sensor.Inicializado,
@ -541,7 +643,9 @@ namespace AgroBase.Services.Operadores
mandatorio = sensor.Mandatorio,
dados,
timestamp = agora,
saude = saude ?? new { }
saude = saude ?? new { },
ultima_resposta_ms = idadeUltimaRespostaSensorMs,
};
senSensoresAtivos.Add($"{tipo}.{id}");
@ -570,6 +674,16 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimoComandoServoMs =
servo.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds;
double idadeUltimaRespostaServoMs =
servo.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - servo.UltimaLeitura).TotalMilliseconds;
var servoObj = new
{
conectado = servo.Inicializado,
@ -581,7 +695,12 @@ namespace AgroBase.Services.Operadores
angulo_sp = servo._AnguloControle,
dados,
timestamp = agora,
saude = saude ?? new { }
saude = saude ?? new { },
angulo_desejado = servo.UltimoAnguloDesejado,
angulo_leitura = servo._AnguloLeiutra,
ultimo_comando_ms = idadeUltimoComandoServoMs,
ultima_resposta_ms = idadeUltimaRespostaServoMs,
};
senServosAtivos.Add($"{tipo}.{id}");
@ -610,6 +729,16 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimoComandoReleMs =
rele.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - rele.UltimoComandoEnviado).TotalMilliseconds;
double idadeUltimaRespostaReleMs =
rele.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - rele.UltimaLeitura).TotalMilliseconds;
var releObj = new
{
conectado = rele.Inicializado,
@ -620,7 +749,12 @@ namespace AgroBase.Services.Operadores
mandatorio = rele.Mandatorio,
dados,
timestamp = agora,
saude = saude ?? new { }
saude = saude ?? new { },
estado_desejado = rele.UltimoEstadoDesejado,
estado_leitura = rele._EstadoLeitura,
ultimo_comando_ms = idadeUltimoComandoReleMs,
ultima_resposta_ms = idadeUltimaRespostaReleMs,
};
senRelesAtivos.Add($"{tipo}.{id}");
@ -649,6 +783,16 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimoComandoLedMs =
led.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds;
double idadeUltimaRespostaLedMs =
led.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - led.UltimaLeitura).TotalMilliseconds;
var ledObj = new
{
conectado = led.Inicializado,
@ -659,7 +803,14 @@ namespace AgroBase.Services.Operadores
mandatorio = led.Mandatorio,
dados,
timestamp = agora,
saude = saude ?? new { }
saude = saude ?? new { },
status_led_id_sp = led.UltimoComportamentoDesejado?.id ?? -1,
status_led_sp = led.UltimoComportamentoDesejado?.statusLED,
status_led_id = led._StatusLedId,
status_led_leitura = led._StatusLed,
ultimo_comando_ms = idadeUltimoComandoLedMs,
ultima_resposta_ms = idadeUltimaRespostaLedMs,
};
senLedsAtivos.Add($"{tipo}.{id}");
@ -669,11 +820,55 @@ namespace AgroBase.Services.Operadores
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Sen), senLedsAtualizados.ToArray());
var DadosAtu = Variaveis.OperacaoEmAndamento.DispAtu.Dados;
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Atu),
DateTime ultimaRespostaAtu =
DadosAtu.DadosLeitura?.UltimoComandoRespondido ?? DateTime.MinValue;
double idadeUltimaRespostaAtuMs =
ultimaRespostaAtu == DateTime.MinValue
? 999999
: (agora_dt - ultimaRespostaAtu).TotalMilliseconds;
bool pulverizadorAutomatico =
Variaveis.OperacaoEmAndamento.Parametros?.Controle?.PulverizadorAutomatico ?? false;
double pressaoAlvo =
Variaveis.OperacaoEmAndamento.Parametros?.Controle?.AtuPressaoLinha ?? 0;
double pressaoLinha =
DadosAtu.PressaoLinha;
bool pressaoLinhaRegular =
pressaoAlvo <= 0 || pressaoLinha >= pressaoAlvo - 20;
RedisService.AtualizarCampos(
RedisService.ModKey(Enums.T_Code.Atu),
("timestamp", agora),
("conectado", DadosAtu.Conectado),
("freq_base", DadosAtu.FrequenciaBase)
("freq_base", DadosAtu.FrequenciaBase),
("ultima_resposta_ms", idadeUltimaRespostaAtuMs),
("latencia_loop_ms", DadosAtu.DadosLeitura?.LatenciaLoop ?? 0),
("pulverizador_automatico", pulverizadorAutomatico),
("pulverizador_liberado", DadosAtu.ContadorCarga.PulverizadorLiberado),
("operacao_iniciada", Variaveis.OperacaoEmAndamento.Iniciado),
("calibrando", Variaveis.OperacaoEmAndamento.Calibrando),
("pausa", Variaveis.OperacaoEmAndamento.Pausa),
("emergencia", Variaveis.OperacaoEmAndamento.Emergencia),
("finalizando", Variaveis.OperacaoEmAndamento.Finalizando),
("percentual_reservatorio", DadosAtu.ContadorCarga.PercentualAtual),
("pressao_linha", pressaoLinha),
("pressao_linha_regular", pressaoLinhaRegular),
("vazao_linha_mls", DadosAtu.VazaoFluxoMLpSReferencia),
("tempo_atuado", DadosAtu.TempoAtuado),
("volume_vazado_ml", DadosAtu.VolumeVazadoML),
("vazao_media_mls", DadosAtu.VazaoMediaMLs),
("bomba_cavitada", _Sensoriamento.Atuador?.BombaCavitada ?? false)
);
var atuSensoresAtualizados = new List<(string caminho, object valor)>();
var atuSensoresAtivos = new List<string>();
@ -698,6 +893,11 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimaRespostaSensorMs =
sensor.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - sensor.UltimaLeitura).TotalMilliseconds;
var sensorObj = new
{
conectado = sensor.Inicializado,
@ -708,7 +908,8 @@ namespace AgroBase.Services.Operadores
mandatorio = sensor.Mandatorio,
dados,
timestamp = agora,
saude = saude ?? new { }
saude = saude ?? new { },
ultima_resposta_ms = idadeUltimaRespostaSensorMs,
};
atuSensoresAtivos.Add($"{tipo}.{id}");
@ -740,6 +941,16 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimoComandoBicoMs =
bico.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - bico.UltimoComandoEnviado).TotalMilliseconds;
double idadeUltimaRespostaBicoMs =
bico.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - bico.UltimaLeitura).TotalMilliseconds;
var bicoObj = new
{
conectado = bico.Inicializado,
@ -750,7 +961,29 @@ namespace AgroBase.Services.Operadores
mandatorio = bico.Mandatorio,
dados,
timestamp = agora,
saude = saude ?? new { }
saude = saude ?? new { },
comando_atuar = bico.ComandoAtuar,
estado_desejado = bico.UltimoStatusDesejado,
angulo_desejado = bico.UltimoAnguloDesejado,
estado_leitura = bico._EstadoLeitura,
angulo_leitura = bico._AnguloBicoLeitura,
angulo_neutro = bico.AnguloNeutro,
angulo_controle = bico.AnguloControle,
angulo_abertura = bico.AnguloAbertura,
ultimo_comando_ms = idadeUltimoComandoBicoMs,
ultima_resposta_ms = idadeUltimaRespostaBicoMs,
atuacoes = bico.Atuacoes,
tempo_atuado = bico.TempoAtuado,
tempo_atuado_em_movimento = bico.TempoAtuadoEmMovimento,
tempo_com_fluxo = bico.TempoComFluxo,
vazao_instantanea_mls = bico.VazaoInstantaneaMLs,
vazao_media_mls = bico.VazaoMediaMLs,
volume_vazado_ml = bico.VolumeVazadoML,
};
atuBicosAtualizados.Add(($"{basePath}", bicoObj));
@ -782,6 +1015,16 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimoComandoBombaMs =
bomba.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - bomba.UltimoComandoEnviado).TotalMilliseconds;
double idadeUltimaRespostaBombaMs =
bomba.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - bomba.UltimaLeitura).TotalMilliseconds;
var sensorObj = new
{
conectado = bomba.Inicializado,
@ -792,7 +1035,34 @@ namespace AgroBase.Services.Operadores
mandatorio = bomba.Mandatorio,
dados,
timestamp = agora,
saude = saude ?? new { }
saude = saude ?? new { },
comando_atuar = bomba.ComandoAtuar,
estado_desejado = bomba.UltimoStatusDesejado,
potencia_desejada = bomba.UltimaPotenciaDesejada,
pressao_desejada = bomba.UltimaPressaoDesejada,
estado_leitura = bomba._EstadoLeitura,
bomba_ligada = bomba._BombaLigada,
potencia_sp_model = bomba.PotenciaSP,
pressao_sp_model = bomba.PressaoSP,
potencia_sp = bomba._PotenciaSP,
potencia = bomba._Potencia,
pressao_sp = bomba._PressaoSP,
pressao = bomba._PressaoAtual,
modo_controle = bomba.Controle?.Modo,
sensor_id_num = bomba.Sensor_ID_Num,
ultimo_comando_ms = idadeUltimoComandoBombaMs,
ultima_resposta_ms = idadeUltimaRespostaBombaMs,
atuacoes = bomba.Atuacoes,
tempo_atuado = bomba.TempoAtuado,
tempo_com_potencia = bomba.TempoComPotencia,
potencia_media_ativa = bomba.PotenciaMediaAtiva,
};
atuBombasAtivos.Add($"{tipo}.{id}");
@ -801,6 +1071,8 @@ namespace AgroBase.Services.Operadores
atuBombasAtualizados.Add(("bombas.ativos", atuBombasAtivos));
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Atu), atuBombasAtualizados.ToArray());
var DadosLra = Variaveis.LoraService;
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Lra),
("timestamp", agora),

View File

@ -3,126 +3,812 @@ from shared.utils import penalizar_frequencia
from shared.contexto_global_redis import ContextoGlobalRedis
from health_worker.modulos.base import ModuloDiagnosticoBase
class ModuloDirecional(ModuloDiagnosticoBase):
"""
Health inteligente do módulo direcional/MKS.
Nova filosofia:
- A frequência dos parâmetros é diagnóstico de telemetria, não juiz principal.
- O que mais importa é: driver vivo, leitura útil recente e comando sendo obedecido.
- Condição operacional com severidade > 90 aparece quando existe risco real
para a operação, porque isso pode bloquear a operação liberada.
"""
SAUDE_MIN_ALERTA = 80
RESPOSTA_RECENTE_MS = 3000.0
RESPOSTA_LENTA_MS = 1800.0
TOLERANCIA_ANGULO_PADRAO_GRAUS = 2.0
COMANDO_GRACE_MS = 900.0
COMANDO_ALERTA_MS = 3000.0
COMANDO_FALHA_MS = 6500.0
ERRO_ALERTA_GRAUS = 5.0
ERRO_FALHA_GRAUS = 10.0
# Penalidade de telemetria deve ser pequena.
# Frequência baixa precisa aparecer no motivo, mas não derrubar módulo obediente.
PENALIDADE_FREQ_MAX_PADRAO = 4
PENALIDADE_FREQ_MAX_CRITICA = 8
def __init__(self):
self.t_code = T_Code.Dir
self.nome = "Direcional"
self.timeout = 3 # segundos
self.timeout = 3
def atualizar_saude(self):
try:
saude_total = 0
total_drivers_em_uso = 0
motivos_dos_ativos = []
dados_dir = ContextoGlobalRedis.get_modulo(self.t_code) or {}
ativos = self._normalizar_ativos(dados_dir.get("ativos", []))
saude_individual = []
updates_drivers = {}
SAUDE_MIN_ALERTA = 80
mod_conectado = False
motivos_gerais = []
condicoes_gerais = []
saude_total_ativos = 0.0
total_drivers_em_uso = 0
algum_conectado = False
dados_dir = ContextoGlobalRedis.get_modulo(self.t_code)
if not ativos:
saude_geral = self._montar_saude_geral(
conectado=False,
status=StatusModulo.DESCONECTADO,
saude=0,
motivos=["Nenhum driver direcional ativo publicado no Redis"],
saude_individual=[],
condicoes_operacionais=[],
)
self._publicar_saude(saude_geral, {})
return
ativos = dados_dir.get("ativos", [])
for endereco in ativos:
endereco = str(endereco) # Redis costuma serializar chave como string
modulo = dados_dir.get(endereco)
endereco_str = str(endereco)
modulo = self._as_dict(
dados_dir.get(endereco_str) or dados_dir.get(endereco)
)
if not modulo:
continue # ativo listado, mas sem dados (race condition)
saude_mod = self._driver_sem_payload(endereco_str)
saude_individual.append(saude_mod)
updates_drivers[f"{endereco_str}.saude"] = saude_mod
continue
saude_mod = self._avaliar_driver(endereco_str, modulo)
#for endereco_str, modulo in dados_dir.items():
# try:
# endereco = int(endereco_str)
# except ValueError:
# continue
mod_id = modulo.get("mod_id")
#print(f"atualizando saude do modulo {mod_id} com endereco {endereco}")
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, 10)
saude -= pen
if freq < FREQ_MIN:
motivos.append(f"Frequencia {label} baixa ({freq:.2f}/{FREQ_BASE} Hz, penalidade {pen}%)")
referenciado = modulo.get("referenciado", False)
angulo = modulo.get("angulo", 0.0)
angulo_sp = modulo.get("angulo_sp", 0.0)
sentido = modulo.get("sentido", Sentido.PARADO.value)
sentido_real = modulo.get("sentido_real", Sentido.PARADO.value)
if referenciado is False:
condicoes_operacionais.append({
"valor": 0,
"severidade": 50,
"descricao": "Driver nunca foi referenciado"
})
if angulo_sp != angulo:
erro_angulo = abs(angulo - angulo_sp)
condicoes_operacionais.append({
"valor": angulo,
"severidade": erro_angulo,
"descricao": f"Angulo incoerente (Atual: {angulo}°, SP: {angulo_sp}°)"
})
if sentido != Sentido.PARADO.value and sentido != sentido_real:
condicoes_operacionais.append({
"valor": angulo,
"severidade": (10 + angulo),
"descricao": f"Direção incoerente (real: {Sentido(sentido_real).name}, esperada: {Sentido(sentido).name}, ângulo: {angulo:.2f}°)"
})
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,
"motivos": motivos,
"condicoes_operacionais": condicoes_operacionais,
"em_uso": comandar
}
saude_individual.append(saude_mod)
# Se o driver está sendo usado, entra na média geral
if comandar is True:
saude_total += saude
total_drivers_em_uso += 1
motivos_dos_ativos.extend([f"Driver {endereco}: {m}" for m in motivos])
modulo["saude"] = saude_mod
updates_drivers[f"{endereco_str}.saude"] = saude_mod
conectado = self._bool(modulo.get("conectado", False))
em_uso = self._bool(saude_mod.get("em_uso", True))
if conectado:
mod_conectado = True
algum_conectado = True
saude_individual.sort(key=lambda x: x["id"])
if em_uso:
saude_total_ativos += self._float(saude_mod.get("saude", 0), 0)
total_drivers_em_uso += 1
saude_final = (saude_total // total_drivers_em_uso) if total_drivers_em_uso > 0 else 0
for motivo in saude_mod.get("motivos", []) or []:
motivos_gerais.append(f"Driver {endereco_str}: {motivo}")
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):
status = StatusModulo.ALERTA
for cond in saude_mod.get("condicoes_operacionais", []) or []:
condicoes_gerais.append({
**cond,
"driver": endereco_str,
"label": saude_mod.get("label"),
})
saude_geral = {
"conectado": mod_conectado,
"status": status.value,
"saude": saude_final,
"motivos": motivos_dos_ativos,
"saude_individual": saude_individual
}
saude_individual.sort(key=lambda x: self._safe_int(x.get("id"), 0))
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.ModKey(self.t_code),
saude=saude_geral
if total_drivers_em_uso > 0:
saude_final = int(round(saude_total_ativos / total_drivers_em_uso))
else:
saude_final = 0
if algum_conectado:
motivos_gerais.append("Nenhum driver direcional marcado como em uso")
status_geral = self._resolver_status_geral(
conectado=algum_conectado,
saude_final=saude_final,
saude_individual=saude_individual,
total_drivers_em_uso=total_drivers_em_uso,
)
saude_geral = self._montar_saude_geral(
conectado=algum_conectado,
status=status_geral,
saude=saude_final,
motivos=self._dedupe(motivos_gerais),
saude_individual=saude_individual,
condicoes_operacionais=condicoes_gerais,
)
self._publicar_saude(saude_geral, updates_drivers)
except Exception as e:
print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}")
try:
self._publicar_saude(
{
"conectado": False,
"status": StatusModulo.FALHA.value,
"saude": 0,
"motivos": [f"Erro interno no health do direcional: {e}"],
"saude_individual": [],
"condicoes_operacionais": [],
},
{},
)
except Exception:
pass
# ============================================================
# Avaliação por driver
# ============================================================
def _avaliar_driver(self, endereco_str, modulo):
mod_id = modulo.get("mod_id")
conectado = self._bool(modulo.get("conectado", False))
comandar = self._bool(modulo.get("comandar", True))
referenciando = self._bool(modulo.get("referenciando", False))
motivos = []
condicoes = []
if not conectado:
return {
"id": endereco_str,
"label": mod_id,
"status": StatusModulo.DESCONECTADO.value,
"saude": 0,
"motivos": ["Desconectado"],
"condicoes_operacionais": [],
"em_uso": comandar,
"componentes": {
"conectividade": 0,
"feedback": 0,
"seguimento_comando": 0,
"telemetria": 0,
"coerencia": 0,
},
"parametros": {},
"debug": self._snapshot_debug(modulo),
}
leitura = self._avaliar_resposta_recente(modulo, motivos, condicoes)
feedback = self._avaliar_feedback(modulo, motivos, condicoes, referenciando)
comando = self._avaliar_seguimento_comando(modulo, motivos, condicoes)
coerencia = self._avaliar_coerencia_operacional(modulo, motivos, condicoes)
telemetria = self._avaliar_telemetria(modulo, motivos, referenciando)
componentes = {
"conectividade": leitura["score"],
"feedback": feedback["score"],
"seguimento_comando": comando["score"],
"coerencia": coerencia["score"],
"telemetria": telemetria["score"],
}
# Pesos intencionais:
# - A telemetria pesa pouco.
# - Se o driver está vivo e obedecendo, frequência baixa vira motivo/alerta local,
# mas não derruba o módulo inteiro.
saude = (
componentes["conectividade"] * 0.25 +
componentes["feedback"] * 0.25 +
componentes["seguimento_comando"] * 0.35 +
componentes["coerencia"] * 0.10 +
componentes["telemetria"] * 0.05
)
# Travas de realidade: alguns casos não podem ser mascarados por média ponderada.
if leitura["sem_resposta"]:
saude = min(saude, 20)
if feedback["sem_pulsos"]:
saude = min(saude, 35)
if comando["falha_seguimento"]:
saude = min(saude, 45)
if coerencia["falha_coerencia"]:
saude = min(saude, 65)
saude = int(max(0, min(100, round(saude))))
status = self._resolver_status_driver(
conectado=conectado,
saude=saude,
condicoes_operacionais=condicoes,
)
return {
"id": endereco_str,
"label": mod_id,
"status": status.value,
"saude": saude,
"motivos": self._dedupe(motivos),
"condicoes_operacionais": condicoes,
"em_uso": comandar,
"referenciando": referenciando,
"componentes": componentes,
"parametros": telemetria["parametros"],
"debug": self._snapshot_debug(modulo),
}
def _driver_sem_payload(self, endereco_str):
return {
"id": endereco_str,
"label": None,
"status": StatusModulo.DESCONECTADO.value,
"saude": 0,
"motivos": ["Driver ativo listado, mas sem payload no Redis"],
"condicoes_operacionais": [],
"em_uso": False,
"componentes": {
"conectividade": 0,
"feedback": 0,
"seguimento_comando": 0,
"telemetria": 0,
"coerencia": 0,
},
"parametros": {},
"debug": {},
}
# ============================================================
# Componentes de saúde
# ============================================================
def _avaliar_resposta_recente(self, modulo, motivos, condicoes):
ultima_resposta_ms = self._float(modulo.get("ultima_resposta_ms", 999999), 999999)
if ultima_resposta_ms > self.RESPOSTA_RECENTE_MS:
motivos.append(
f"Sem resposta recente do MKS ({ultima_resposta_ms:.0f} ms)"
)
condicoes.append({
"valor": ultima_resposta_ms,
"severidade": 95,
"descricao": f"Driver direcional sem resposta recente ({ultima_resposta_ms:.0f} ms)",
})
return {
"score": 0,
"sem_resposta": True,
}
if ultima_resposta_ms > self.RESPOSTA_LENTA_MS:
motivos.append(
f"Resposta CAN lenta ({ultima_resposta_ms:.0f} ms)"
)
return {
"score": 75,
"sem_resposta": False,
}
return {
"score": 100,
"sem_resposta": False,
}
def _avaliar_feedback(self, modulo, motivos, condicoes, referenciando):
freqs = self._normalizar_dict_chaves(modulo.get("freq", {}))
freq_pulsos = self._float(freqs.get("pulsosrecebidos", 0.0), 0.0)
freq_io = self._float(freqs.get("io", 0.0), 0.0)
angulo = self._float(modulo.get("angulo", 0.0), 0.0)
angulo_sp = self._float(modulo.get("angulo_sp", 0.0), 0.0)
# Pulsos é o feedback central do MKS.
if freq_pulsos <= 0:
motivos.append("Sem feedback de PulsosRecebidos")
condicoes.append({
"valor": freq_pulsos,
"severidade": 92,
"descricao": "Sem leitura de PulsosRecebidos, não há feedback confiável de ângulo",
})
return {
"score": 0,
"sem_pulsos": True,
}
score = 100
# IO é crítico durante referenciamento, mas só auxiliar na operação.
if referenciando and freq_io <= 0:
motivos.append("Referenciando sem leitura de IO")
condicoes.append({
"valor": freq_io,
"severidade": 91,
"descricao": "Referenciamento sem leitura de IO do sensor do rasgo",
})
score = min(score, 30)
elif freq_io <= 0:
motivos.append("Sem leitura de IO; sentido real pode ficar desatualizado")
score = min(score, 85)
return {
"score": score,
"sem_pulsos": False,
"angulo": angulo,
"angulo_sp": angulo_sp,
}
def _avaliar_seguimento_comando(self, modulo, motivos, condicoes):
comandar = self._bool(modulo.get("comandar", True))
atingiu = self._bool(modulo.get("atingiu_angulo_sp", False))
retornando_a_zero = self._bool(modulo.get("retornando_a_zero", False))
erro_angulo = self._float(
modulo.get(
"erro_angulo",
abs(
self._float(modulo.get("angulo", 0.0), 0.0) -
self._float(modulo.get("angulo_sp", 0.0), 0.0)
)
),
0.0,
)
ultimo_comando_ms = self._float(modulo.get("ultimo_comando_ms", 999999), 999999)
ultima_direcao = self._safe_int(
modulo.get("ultima_direcao", self._direcao_parada_value()),
self._direcao_parada_value(),
)
if not comandar:
# Driver conectado, mas não participa do comando atual.
return {
"score": 100,
"falha_seguimento": False,
}
if atingiu:
return {
"score": 100,
"falha_seguimento": False,
}
if ultimo_comando_ms < self.COMANDO_GRACE_MS:
return {
"score": 96,
"falha_seguimento": False,
}
if retornando_a_zero and erro_angulo <= self.ERRO_ALERTA_GRAUS:
motivos.append(
f"Retornando a zero com erro residual ({erro_angulo:.2f}°)"
)
return {
"score": 88,
"falha_seguimento": False,
}
# Aqui começa a diferença entre telemetria lenta e comando que não pegou.
if ultimo_comando_ms >= self.COMANDO_FALHA_MS and erro_angulo >= self.ERRO_FALHA_GRAUS:
motivos.append(
f"Comando direcional não convergiu "
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
)
condicoes.append({
"valor": erro_angulo,
"severidade": 92,
"descricao": (
"Driver direcional não acompanhou o comando "
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
),
})
return {
"score": 20,
"falha_seguimento": True,
}
if ultimo_comando_ms >= self.COMANDO_ALERTA_MS and erro_angulo >= self.ERRO_ALERTA_GRAUS:
motivos.append(
f"Ângulo ainda distante do SP "
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
)
condicoes.append({
"valor": erro_angulo,
"severidade": min(89, max(40, erro_angulo * 5)),
"descricao": (
"Ângulo direcional distante do SP, mas ainda dentro de janela de alerta "
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
),
})
return {
"score": 70,
"falha_seguimento": False,
}
if erro_angulo > self.TOLERANCIA_ANGULO_PADRAO_GRAUS:
motivos.append(
f"Ângulo fora da tolerância momentaneamente (erro={erro_angulo:.2f}°)"
)
return {
"score": 90,
"falha_seguimento": False,
}
return {
"score": 100,
"falha_seguimento": False,
}
def _avaliar_coerencia_operacional(self, modulo, motivos, condicoes):
referenciado = self._bool(modulo.get("referenciado", False))
referenciando = self._bool(modulo.get("referenciando", False))
sentido = self._safe_int(
modulo.get("sentido", self._sentido_parado_value()),
self._sentido_parado_value(),
)
sentido_real = self._safe_int(
modulo.get("sentido_real", self._sentido_parado_value()),
self._sentido_parado_value(),
)
sentido_sp = self._safe_int(
modulo.get("sentido_sp", self._sentido_parado_value()),
self._sentido_parado_value(),
)
score = 100
falha_coerencia = False
if not referenciado and not referenciando:
motivos.append("Driver ainda não foi referenciado")
condicoes.append({
"valor": 0,
"severidade": 50,
"descricao": "Driver direcional ainda não foi referenciado",
})
score = min(score, 75)
# Sentido real vem do sensor do rasgo.
# Como esse sensor pode ter borda/transição, não vamos bloquear operação por isso sozinho.
if sentido != self._sentido_parado_value() and sentido != sentido_real:
motivos.append(
f"Sentido real incoerente "
f"(real={self._nome_sentido(sentido_real)}, esperado={self._nome_sentido(sentido)})"
)
condicoes.append({
"valor": sentido_real,
"severidade": 70,
"descricao": (
"Sentido real difere do sentido calculado "
f"(real={self._nome_sentido(sentido_real)}, "
f"esperado={self._nome_sentido(sentido)}, "
f"SP={self._nome_sentido(sentido_sp)})"
),
})
score = min(score, 70)
return {
"score": score,
"falha_coerencia": falha_coerencia,
}
def _avaliar_telemetria(self, modulo, motivos, referenciando):
parametros = self._resolver_parametros_monitorados(modulo)
parametros_debug = {}
if not parametros:
motivos.append("Contrato de telemetria sem parâmetros em uso")
return {
"score": 70,
"parametros": {},
}
scores = []
for parametro in parametros:
avaliacao = self._avaliar_frequencia_parametro(modulo, parametro, referenciando)
parametros_debug[parametro] = avaliacao
freq = avaliacao["freq"]
freq_min = avaliacao["freq_min"]
freq_base = avaliacao["freq_base"]
score_param = avaliacao["score"]
scores.append(score_param)
if freq <= 0:
motivos.append(f"Telemetria {parametro} ausente (0 Hz)")
elif freq < freq_min:
motivos.append(
f"Telemetria {parametro} baixa "
f"({freq:.2f}/{freq_base:.2f} Hz, min={freq_min:.2f} Hz)"
)
score = sum(scores) / len(scores) if scores else 70
return {
"score": int(max(0, min(100, round(score)))),
"parametros": parametros_debug,
}
# ============================================================
# Frequência / contrato
# ============================================================
def _resolver_parametros_monitorados(self, modulo):
freq_uso = self._normalizar_dict_chaves(modulo.get("freq_uso", {}))
polling = self._as_dict(modulo.get("polling", {}))
parametros = [
parametro
for parametro, em_uso in freq_uso.items()
if self._bool(em_uso)
]
if parametros:
return sorted(parametros)
ordem = polling.get("ordem", [])
if isinstance(ordem, list) and ordem:
return sorted([
self._canon(p)
for p in ordem
if str(p).strip()
])
freqs = self._normalizar_dict_chaves(modulo.get("freq", {}))
return sorted(freqs.keys())
def _avaliar_frequencia_parametro(self, modulo, parametro, referenciando):
freqs = self._normalizar_dict_chaves(modulo.get("freq", {}))
freq_base = self._normalizar_dict_chaves(modulo.get("freq_base", {}))
freq_min = self._normalizar_dict_chaves(modulo.get("freq_min", {}))
polling_ms = self._normalizar_dict_chaves(modulo.get("polling_ms", {}))
freq = self._float(freqs.get(parametro, 0.0), 0.0)
base = self._float(freq_base.get(parametro, 0.0), 0.0)
minimo = self._float(freq_min.get(parametro, 0.0), 0.0)
ms = self._float(polling_ms.get(parametro, 0.0), 0.0)
if base <= 0 and ms > 0:
# Fallback. O contrato novo deveria publicar freq_base.
base = 1000.0 / ms
if base <= 0:
base = 1.0
if minimo <= 0:
minimo = base * 0.40
critico = parametro == "pulsosrecebidos" or (referenciando and parametro == "io")
penalidade_max = self.PENALIDADE_FREQ_MAX_CRITICA if critico else self.PENALIDADE_FREQ_MAX_PADRAO
penalidade = float(
penalizar_frequencia(freq, base, minimo, penalidade_max) or 0
)
# Transformado em score local da telemetria. Não derruba sozinho o driver.
score = max(0, min(100, 100 - (penalidade * 4)))
if freq <= 0:
score = 0 if critico else 55
return {
"freq": freq,
"freq_base": base,
"freq_min": minimo,
"polling_ms": ms,
"critico": critico,
"penalidade": penalidade,
"score": score,
"ok": freq >= minimo,
}
# ============================================================
# Status
# ============================================================
def _resolver_status_driver(self, conectado, saude, condicoes_operacionais):
if not conectado:
return StatusModulo.DESCONECTADO
if any(self._float(c.get("severidade", 0), 0) > 90 for c in condicoes_operacionais):
return StatusModulo.FALHA
if saude <= 0:
return StatusModulo.FALHA
if saude < self.SAUDE_MIN_ALERTA:
return StatusModulo.ALERTA
return StatusModulo.OPERANTE
def _resolver_status_geral(self, conectado, saude_final, saude_individual, total_drivers_em_uso):
if not conectado:
return StatusModulo.DESCONECTADO
if total_drivers_em_uso <= 0:
return StatusModulo.ALERTA
in_uso = [
item for item in saude_individual
if self._bool(item.get("em_uso", False))
]
if any(item.get("status") == StatusModulo.FALHA.value for item in in_uso):
return StatusModulo.FALHA
if saude_final <= 0:
return StatusModulo.FALHA
if any(item.get("status") == StatusModulo.ALERTA.value for item in in_uso):
return StatusModulo.ALERTA
if saude_final < self.SAUDE_MIN_ALERTA:
return StatusModulo.ALERTA
return StatusModulo.OPERANTE
def _montar_saude_geral(self, conectado, status, saude, motivos, saude_individual, condicoes_operacionais):
return {
"conectado": conectado,
"status": status.value,
"saude": int(max(0, min(100, saude))),
"motivos": self._dedupe(motivos),
"saude_individual": saude_individual,
"condicoes_operacionais": condicoes_operacionais,
}
def _publicar_saude(self, saude_geral, updates_drivers):
payload = {"saude": saude_geral}
payload.update(updates_drivers or {})
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.ModKey(self.t_code),
**payload,
)
# ============================================================
# Debug / helpers
# ============================================================
def _snapshot_debug(self, modulo):
return {
"angulo": self._float(modulo.get("angulo", 0.0), 0.0),
"angulo_sp": self._float(modulo.get("angulo_sp", 0.0), 0.0),
"erro_angulo": self._float(modulo.get("erro_angulo", 0.0), 0.0),
"atingiu_angulo_sp": self._bool(modulo.get("atingiu_angulo_sp", False)),
"ultimo_comando_ms": self._float(modulo.get("ultimo_comando_ms", 999999), 999999),
"ultima_resposta_ms": self._float(modulo.get("ultima_resposta_ms", 999999), 999999),
"ultima_direcao": modulo.get("ultima_direcao"),
"status_movimento": modulo.get("status_movimento"),
"rpm": self._float(modulo.get("rpm", 0.0), 0.0),
"em_movimento": self._bool(modulo.get("em_movimento", False)),
"retornando_a_zero": self._bool(modulo.get("retornando_a_zero", False)),
"referenciado": self._bool(modulo.get("referenciado", False)),
"referenciando": self._bool(modulo.get("referenciando", False)),
"sentido": modulo.get("sentido"),
"sentido_sp": modulo.get("sentido_sp"),
"sentido_real": modulo.get("sentido_real"),
}
@staticmethod
def _normalizar_ativos(ativos):
if not isinstance(ativos, list):
return []
normalizados = []
for item in ativos:
try:
normalizados.append(int(item))
except Exception:
try:
normalizados.append(int(str(item).strip()))
except Exception:
pass
return normalizados
@staticmethod
def _as_dict(valor):
return valor if isinstance(valor, dict) else {}
@staticmethod
def _canon(valor):
texto = str(valor or "").strip().lower()
return "".join(ch for ch in texto if ch.isalnum())
def _normalizar_dict_chaves(self, valor):
if not isinstance(valor, dict):
return {}
return {
self._canon(k): v
for k, v in valor.items()
if str(k).strip()
}
@staticmethod
def _bool(valor):
try:
if isinstance(valor, bool):
return valor
if valor is None:
return False
if isinstance(valor, (int, float)):
return valor != 0
texto = str(valor).strip().lower()
if texto in ["true", "1", "sim", "yes", "y", "on"]:
return True
if texto in ["false", "0", "nao", "não", "no", "n", "off", ""]:
return False
return bool(valor)
except Exception:
return False
@staticmethod
def _float(valor, padrao=0.0):
try:
if valor is None:
return float(padrao)
return float(valor)
except Exception:
return float(padrao)
@staticmethod
def _safe_int(valor, padrao=0):
try:
if valor is None:
return int(padrao)
return int(valor)
except Exception:
return int(padrao)
@staticmethod
def _dedupe(lista):
saida = []
vistos = set()
for item in lista or []:
texto = str(item)
if texto in vistos:
continue
vistos.add(texto)
saida.append(item)
return saida
@staticmethod
def _enum_value(enum_cls, nomes, padrao=0):
for nome in nomes:
try:
if hasattr(enum_cls, nome):
return getattr(enum_cls, nome).value
except Exception:
pass
return padrao
def _sentido_parado_value(self):
return self._enum_value(Sentido, ["PARADO", "Parado"], 0)
def _direcao_parada_value(self):
# Direcao não está importado neste módulo.
# Nos enums do C# normalmente Parado = 0.
return 0
@staticmethod
def _nome_sentido(valor):
try:
return Sentido(valor).name
except Exception:
return str(valor)