diff --git a/AgroBase/AgroBase/Forms/IHM/frmDiagnosticos.cs b/AgroBase/AgroBase/Forms/IHM/frmDiagnosticos.cs index 497b8c2a2..09582dfac 100644 --- a/AgroBase/AgroBase/Forms/IHM/frmDiagnosticos.cs +++ b/AgroBase/AgroBase/Forms/IHM/frmDiagnosticos.cs @@ -1267,7 +1267,7 @@ namespace AgroBase.Forms.IHM idx = PreencheDadosLinha(grid, Testes, idx, Corrente12V); var SImu = Variaveis.OperacaoEmAndamento.Sensoriamento.IMU; - double Orientacao = SImu.Iniciado ? SImu.Rotacao : -1; + double Orientacao = SImu.Iniciado ? SImu.YawSeguro : -1; idx = PreencheDadosLinha(grid, Testes, idx, Orientacao); bool GpsConectado = SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Enums.T_Code.Gps); diff --git a/AgroBase/AgroBase/Forms/IHM/frmDialogoMapa.cs b/AgroBase/AgroBase/Forms/IHM/frmDialogoMapa.cs index d8f3bcc58..089a20cc8 100644 --- a/AgroBase/AgroBase/Forms/IHM/frmDialogoMapa.cs +++ b/AgroBase/AgroBase/Forms/IHM/frmDialogoMapa.cs @@ -217,7 +217,7 @@ namespace AgroBase.Forms.IHM txtDentroCorredor.Text = _Sensoriamento.Trajetoria.CorredorAtual.Dentro ? "Sim" : "Não"; txtMargemCorredor.Text = _Sensoriamento.Trajetoria.NaMargemDoCorredor ? "Sim" : "Não"; txtStatusCarro.Text = _Sensoriamento.Trajetoria.StatusCarro.ToString(); - txtInclinacao.Text = _Sensoriamento.IMU.InclinacaoLateral.ToString("0.00") + "/" + _Sensoriamento.IMU.InclinacaoFrontal.ToString("0.00"); + txtInclinacao.Text = _Sensoriamento.IMU.RollSeguro.ToString("0.00") + "/" + _Sensoriamento.IMU.PitchSeguro.ToString("0.00"); var ModET = Variaveis.OperacaoEmAndamento.DispMvd.Dados.Modulos.FirstOrDefault(x => x.Modulo_ID == "ET"); txtET.Text = (ModET?.MovMotor?.Corrente_Motor ?? 0).ToString("0.00"); diff --git a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs index 9065369db..6d6d437fa 100644 --- a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs +++ b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs @@ -840,7 +840,7 @@ namespace AgroBase.Models.Modules CanManager.CanService.RegistrarHandler(_EnderecoCAN_Rx, _AtuHandler); - RequisitarDadosModulo(Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Status, CanMessagePosicaoDados.Status, true); + RequisitarDadosDescoberta(); DateTime Inicio = DateTime.Now; bool Respondido() => Variaveis.OperacaoEmAndamento.DispAtu.Dados.DadosLeitura.UltimoComandoRespondido > Inicio; @@ -872,6 +872,11 @@ namespace AgroBase.Models.Modules } } + private void RequisitarDadosDescoberta() + { + Variaveis.OperacaoEmAndamento.DispAtu?.AdicionarMensagemFila(Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Status, CanMessagePosicaoDados.Status, null, true, false, true); + } + public void RequisitarDadosModulo(int ID_Num, CanMessagePosicaoDados posicaoTx, CanMessagePosicaoDados posicaoRx, bool aguardaResposta) { Variaveis.OperacaoEmAndamento.DispAtu?.AdicionarMensagemFila(ID_Num, posicaoTx, posicaoRx, null, aguardaResposta); diff --git a/AgroBase/AgroBase/Models/Modules/DirecionalModel.cs b/AgroBase/AgroBase/Models/Modules/DirecionalModel.cs index 29b25e238..ec0cc8464 100644 --- a/AgroBase/AgroBase/Models/Modules/DirecionalModel.cs +++ b/AgroBase/AgroBase/Models/Modules/DirecionalModel.cs @@ -530,7 +530,7 @@ namespace AgroBase.Models.Modules public void RequisitarDadosSensoresExternos() { - if (SensorAnguloReal != null) + if (SensorAnguloReal?.Inicializado == true && SensorAnguloReal?.Aferir == true) { Variaveis.OperacaoEmAndamento.DispSen.Dados.RequisitarDadosModulo(SensorAnguloReal.ID_Num, CanMessagePosicaoDados.Dados1, CanMessagePosicaoDados.Dados1, true); } diff --git a/AgroBase/AgroBase/Models/Modules/MovimentacaoModel.cs b/AgroBase/AgroBase/Models/Modules/MovimentacaoModel.cs index 5b97e7797..d24764726 100644 --- a/AgroBase/AgroBase/Models/Modules/MovimentacaoModel.cs +++ b/AgroBase/AgroBase/Models/Modules/MovimentacaoModel.cs @@ -458,7 +458,7 @@ namespace AgroBase.Models.Modules public void RequisitarDadosSensoresExternos() { - if (SensorTemperaturaMotor != null) + if (SensorTemperaturaMotor?.Inicializado == true && SensorTemperaturaMotor?.Aferir == true) { Variaveis.OperacaoEmAndamento.DispSen.Dados.RequisitarDadosModulo(SensorTemperaturaMotor.ID_Num, CanMessagePosicaoDados.Dados1, CanMessagePosicaoDados.Dados1, true); } diff --git a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs index 7bc57186b..009c56516 100644 --- a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs +++ b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs @@ -2100,7 +2100,7 @@ namespace AgroBase.Models.Modules CanManager.CanService.RegistrarHandler(_EnderecoCAN_Rx, _SenHandler); - RequisitarDadosModulo(Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Status, CanMessagePosicaoDados.Status, true); + RequisitarDadosDescoberta(); DateTime Inicio = DateTime.Now; bool Respondido() => Variaveis.OperacaoEmAndamento.DispSen.Dados.DadosLeitura.UltimoComandoRespondido > Inicio; @@ -2132,6 +2132,11 @@ namespace AgroBase.Models.Modules } } + private void RequisitarDadosDescoberta() + { + Variaveis.OperacaoEmAndamento.DispSen?.AdicionarMensagemFila(Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Status, CanMessagePosicaoDados.Status, null, true, false, true); + } + public void RequisitarDadosModulo(int ID_Num, CanMessagePosicaoDados posicaoTx, CanMessagePosicaoDados posicaoRx, bool aguardaResposta) { Variaveis.OperacaoEmAndamento.DispSen?.AdicionarMensagemFila(ID_Num, posicaoTx, posicaoRx, null, aguardaResposta); diff --git a/AgroBase/AgroBase/Services/CANService.cs b/AgroBase/AgroBase/Services/CANService.cs index d91e8f57f..329f5beef 100644 --- a/AgroBase/AgroBase/Services/CANService.cs +++ b/AgroBase/AgroBase/Services/CANService.cs @@ -1,5 +1,6 @@ using AgroBase.Models; using System; +using System.Collections.Generic; using System.IO.Ports; using static AgroBase.Models.Enums; @@ -148,6 +149,9 @@ namespace AgroBase.Services public string ChaveInterna { get; set; } public bool IsExtended => IdRx > 0x7FF || IdTx > 0x7FF; + + public bool ContabilizarTimeoutHealth { get; set; } = true; + public bool MensagemDescoberta { get; set; } = false; } public class CanCommandFreqModel @@ -262,7 +266,7 @@ namespace AgroBase.Services bool PortaIsCan(SerialPort Porta = null); void RegistrarHandler(uint endereco_rx, ICanMessageHandler handler); bool Inicializar(int BitRate = 250); - void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] idTx, byte[] idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true); + void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] idTx, byte[] idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true, bool contabilizarTimeoutHealth = true, bool mensagemDescoberta = false); void Fechar(); string _portName { get; set; } @@ -278,6 +282,7 @@ namespace AgroBase.Services int ErrosCriticos { get; set; } double UltimoErroCritico { get; set; } + string UltimoErroCriticoMotivo { get; } bool FilaLiberada { get; } @@ -286,6 +291,11 @@ namespace AgroBase.Services long UltimoRxUnixMs { get; } long UltimoErroCriticoUnixMs { get; } + Dictionary TimeoutsRespostaPorId { get; } + Dictionary TimeoutsRespostaPorDispositivo { get; } + Dictionary TimeoutsRespostaPorChave { get; } + Dictionary PendentesAguardandoRespostaPorId { get; } + long TxTotal { get; } long RxTotal { get; } long TxFalhaTotal { get; } diff --git a/AgroBase/AgroBase/Services/CANServiceSerial.cs b/AgroBase/AgroBase/Services/CANServiceSerial.cs index dc5cefb8f..151c859cf 100644 --- a/AgroBase/AgroBase/Services/CANServiceSerial.cs +++ b/AgroBase/AgroBase/Services/CANServiceSerial.cs @@ -32,7 +32,7 @@ namespace AgroBase.Services private readonly ConcurrentDictionary _roteadoresRegistrados = new ConcurrentDictionary(); private uint? _ultimoIdEnviado = null; - private List Mensagens = new List(); + private List MensagensPendentes = new List(); private readonly object _LockMensagens = new object(); public bool FilaLiberada @@ -42,9 +42,9 @@ namespace AgroBase.Services lock (_LockMensagens) { bool liberado = - Mensagens == null || - Mensagens.Count == 0 || - !Mensagens.Any(x => !x.Enviado && x.Momento >= DateTime.Now.AddSeconds(-10)); + MensagensPendentes == null || + MensagensPendentes.Count == 0 || + !MensagensPendentes.Any(x => !x.Enviado && x.Momento >= DateTime.Now.AddSeconds(-10)); return liberado; } } @@ -58,7 +58,37 @@ namespace AgroBase.Services public double FreqRx{ get; set; } = 0; public int ErrosCriticos { get; set; } = 0; public double UltimoErroCritico { get; set; } = 0; + public string UltimoErroCriticoMotivo { get; private set; } = ""; private DateTime _ultimoErroCritico = DateTime.MinValue; + private DateTime _portaIndisponivelDesde = DateTime.MinValue; + private DateTime _reaberturaSolicitadaDesde = DateTime.MinValue; + + private readonly ConcurrentDictionary _timeoutsRespostaPorId = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _timeoutsRespostaPorDispositivo = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _timeoutsRespostaPorChave = new ConcurrentDictionary(); + + public Dictionary TimeoutsRespostaPorId => + _timeoutsRespostaPorId.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary TimeoutsRespostaPorDispositivo => + _timeoutsRespostaPorDispositivo.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary TimeoutsRespostaPorChave => + _timeoutsRespostaPorChave.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary PendentesAguardandoRespostaPorId + { + get + { + lock (_LockMensagens) + { + return MensagensPendentes + .Where(x => x.ComResposta && x.Enviado && !x.Respondido) + .GroupBy(x => IdKey(x.IdRx)) + .ToDictionary(g => g.Key, g => g.Count()); + } + } + } public long UltimoTxUnixMs { get; private set; } = 0; public long UltimoRxUnixMs { get; private set; } = 0; @@ -94,7 +124,7 @@ namespace AgroBase.Services get { lock (_LockMensagens) - return Mensagens?.Count ?? 0; + return MensagensPendentes?.Count ?? 0; } } @@ -103,7 +133,7 @@ namespace AgroBase.Services get { lock (_LockMensagens) - return Mensagens?.Count(x => !x.Enviado) ?? 0; + return MensagensPendentes?.Count(x => !x.Enviado) ?? 0; } } @@ -112,14 +142,14 @@ namespace AgroBase.Services get { lock (_LockMensagens) - return Mensagens?.Count(x => x.ComResposta && x.Enviado && !x.Respondido) ?? 0; + return MensagensPendentes?.Count(x => x.ComResposta && x.Enviado && !x.Respondido) ?? 0; } } public int FramesRxNaFila => FramesRecebidos?.Count ?? 0; private bool DebugMode = false; - private void MostrarLog(string message, uint id) + private void MostrarLog(string message, uint id = 0) { if (DebugMode) // && new List() { 0x18100140 }.Contains(id) { @@ -492,7 +522,7 @@ namespace AgroBase.Services lock (_LockMensagens) { - mensagem = Mensagens.FirstOrDefault(x => + mensagem = MensagensPendentes.FirstOrDefault(x => { if (!x.Enviado) return false; if (x.Respondido) return false; @@ -567,7 +597,7 @@ namespace AgroBase.Services } } - public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true) + public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true, bool contabilizarTimeoutHealth = true, bool mensagemDescoberta = false) { if (_PortaCAN?.IsOpen != true) return; @@ -604,10 +634,10 @@ namespace AgroBase.Services lock (_LockMensagens) { - mensagemNaFila = Mensagens.Any(x => (!x.Enviado || (x.ComResposta && x.Enviado && !x.Respondido)) && x.IdTx == idTx && x.IdRx == idRx && x.DataTx.SequenceEqual(dados.ToArray())); + mensagemNaFila = MensagensPendentes.Any(x => (!x.Enviado || (x.ComResposta && x.Enviado && !x.Respondido)) && x.IdTx == idTx && x.IdRx == idRx && x.DataTx.SequenceEqual(dados.ToArray())); if (!mensagemNaFila) { - Mensagens.Add(novaMensagem); + MensagensPendentes.Add(novaMensagem); MostrarLog($"Mensagem adicionada na fila. ID: {novaMensagem.IdTx}, Data: {FuncoesGlobais.ConverterComandoBytesParaTexto(novaMensagem.DataTx)}", idTx); //if (dados[1] == Variaveis.ID_Num_sLRA) //{ @@ -627,7 +657,7 @@ namespace AgroBase.Services lock (_LockMensagens) { - mensagensPendentes = Mensagens + mensagensPendentes = MensagensPendentes .Where(x => !x.Enviado) .ToList(); } @@ -806,7 +836,7 @@ namespace AgroBase.Services lock (_LockMensagens) { - var timeouts = Mensagens + var timeouts = MensagensPendentes .Where(x => x.ComResposta && x.Enviado && @@ -816,11 +846,11 @@ namespace AgroBase.Services foreach (var item in timeouts) { - TimeoutsRespostaTotal++; - Mensagens.Remove(item); + RegistrarTimeoutResposta(item); + MensagensPendentes.Remove(item); } - var remover = Mensagens + var remover = MensagensPendentes .Where(x => (!x.ComResposta && x.Enviado) || (x.ComResposta && x.Respondido) || @@ -828,7 +858,7 @@ namespace AgroBase.Services .ToList(); foreach (var item in remover) - Mensagens.Remove(item); + MensagensPendentes.Remove(item); } } @@ -841,17 +871,6 @@ namespace AgroBase.Services var portaExiste = !string.IsNullOrEmpty(_portName) && portas.Contains(_portName, StringComparer.OrdinalIgnoreCase); - if (!portaExiste || _PortaCAN == null || !_PortaCAN.IsOpen) - { - IsConnected = false; - RegistrarErroCritico("porta_serial_indisponivel"); - - Task.Run(() => SerialService.RealizarVarreduraPortasUSB()); - return; - } - - bool existeDemanda = ExisteDemandaCan(); - double ageTx = _ultimoTx == DateTime.MinValue ? 999999 : (agora - _ultimoTx).TotalSeconds; @@ -860,36 +879,76 @@ namespace AgroBase.Services ? 999999 : (agora - _ultimoRx).TotalSeconds; - if (IsConnected && _precisaReabrir) + bool portaOk = portaExiste && _PortaCAN != null && _PortaCAN.IsOpen; + + if (!portaOk) { - _precisaReabrir = false; + if (_portaIndisponivelDesde == DateTime.MinValue) + _portaIndisponivelDesde = agora; - RegistrarErroCritico("serial_solicitou_reabertura"); + double tempoSuspeito = (agora - _portaIndisponivelDesde).TotalSeconds; + bool semTrafegoRecente = ageTx > 2.0 && ageRx > 2.0; - MostrarLog("Reabrindo conexão CAN por evento da serial...", 0); + if (tempoSuspeito >= 1.0 && semTrafegoRecente) + { + IsConnected = false; + RegistrarErroCritico("porta_serial_indisponivel_confirmada"); - Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog( - T_Code.Can, - StatusModulo.Alerta, - 50, - "Reabrindo conexão CAN por evento da serial..." - ); + Task.Run(() => SerialService.RealizarVarreduraPortasUSB()); + } - ReabrirPortaSegura(); return; } - if (IsConnected && existeDemanda && ageTx > 2.0 && ageRx > 2.0) - { - RegistrarErroCritico("sem_trafego_com_demanda"); + _portaIndisponivelDesde = DateTime.MinValue; - MostrarLog("Muito tempo sem tráfego CAN Serial com demanda pendente, reiniciando conexão...", 0); + bool existeDemanda = ExisteDemandaCan(); + + if (IsConnected && _precisaReabrir) + { + if (_reaberturaSolicitadaDesde == DateTime.MinValue) + _reaberturaSolicitadaDesde = agora; + + double tempoSolicitado = (agora - _reaberturaSolicitadaDesde).TotalSeconds; + + // Evento explícito de serial é mais forte que watchdog de silêncio, + // mas ainda dá uma pequena janela para evitar falso positivo espirrado. + if (tempoSolicitado >= 0.5) + { + _precisaReabrir = false; + _reaberturaSolicitadaDesde = DateTime.MinValue; + + RegistrarErroCritico("serial_solicitou_reabertura"); + + MostrarLog("Reabrindo conexão CAN por evento da serial...", 0); + + Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog( + T_Code.Can, + StatusModulo.Alerta, + 50, + "Reabrindo conexão CAN por evento da serial..." + ); + + ReabrirPortaSegura(); + return; + } + } + else + { + _reaberturaSolicitadaDesde = DateTime.MinValue; + } + + if (IsConnected && existeDemanda && ageTx > 10.0 && ageRx > 10.0) + { + RegistrarErroCritico("sem_trafego_prolongado_com_demanda"); + + MostrarLog("Sem tráfego prolongado no CAN Serial com demanda, reiniciando conexão...", 0); Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog( T_Code.Can, StatusModulo.Alerta, 50, - "Muito tempo sem tráfego CAN Serial com demanda pendente, reiniciando conexão..." + "Sem tráfego prolongado no CAN Serial com demanda, reiniciando conexão..." ); ReabrirPortaSegura(); @@ -911,7 +970,7 @@ namespace AgroBase.Services lock (_LockMensagens) { - Mensagens.Clear(); + MensagensPendentes.Clear(); } SerialCANBuffer.Clear(); @@ -1001,7 +1060,7 @@ namespace AgroBase.Services lock (_LockMensagens) { - Mensagens.Clear(); + MensagensPendentes.Clear(); } lock (_lock) @@ -1088,7 +1147,11 @@ namespace AgroBase.Services UltimoErroCritico = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency; UltimoErroCriticoUnixMs = NowUnixMs(); - MostrarLog($"Erro crítico: {motivo}", 0); + UltimoErroCriticoMotivo = motivo; + + MostrarLog($"Erro crítico: {motivo}"); + var saude = Variaveis.OperacaoEmAndamento.Sensoriamento.ModulosSaude.FirstOrDefault(x => x.modulo == T_Code.Can); + Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog(T_Code.Can, saude?.status ?? StatusModulo.Desconectado, saude?.saude ?? 0, $"Erro crítico: {motivo}"); } private void RegistrarLatenciaResposta(double latenciaMs) @@ -1139,11 +1202,45 @@ namespace AgroBase.Services { lock (_LockMensagens) { - return Mensagens.Any(x => + return MensagensPendentes.Any(x => !x.Enviado || (x.ComResposta && x.Enviado && !x.Respondido)); } } + private static string IdKey(uint id) + { + return $"0x{id:X}"; + } + + private static string TimeoutChave(CanMessage msg) + { + if (msg == null) + return "desconhecido"; + + return $"{msg.Dispositivo}|tx:{IdKey(msg.IdTx)}|rx:{IdKey(msg.IdRx)}|fc_rx:0x{msg.funcCodeRx:X2}"; + } + + private void IncrementarContador(ConcurrentDictionary dict, string key) + { + dict.AddOrUpdate(key, 1, (_, atual) => atual + 1); + } + + private void RegistrarTimeoutResposta(CanMessage msg) + { + TimeoutsRespostaTotal++; + + string idRx = IdKey(msg.IdRx); + string dispositivo = msg.Dispositivo.ToString(); + string chave = TimeoutChave(msg); + + IncrementarContador(_timeoutsRespostaPorId, idRx); + IncrementarContador(_timeoutsRespostaPorDispositivo, dispositivo); + IncrementarContador(_timeoutsRespostaPorChave, chave); + + MostrarLog($"Timeout resposta CAN | {chave}", msg.IdRx); + } + + } } \ No newline at end of file diff --git a/AgroBase/AgroBase/Services/CANServiceWaveshare.cs b/AgroBase/AgroBase/Services/CANServiceWaveshare.cs index b40b74d97..0cf18f2b2 100644 --- a/AgroBase/AgroBase/Services/CANServiceWaveshare.cs +++ b/AgroBase/AgroBase/Services/CANServiceWaveshare.cs @@ -55,7 +55,36 @@ namespace AgroBase.Services public double FreqRx { get; set; } = 0; public int ErrosCriticos { get; set; } = 0; public double UltimoErroCritico { get; set; } = 0; + public string UltimoErroCriticoMotivo { get; private set; } = ""; private DateTime _ultimoErroCritico = DateTime.MinValue; + private DateTime _connectorIndisponivelDesde = DateTime.MinValue; + + private readonly ConcurrentDictionary _timeoutsRespostaPorId = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _timeoutsRespostaPorDispositivo = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _timeoutsRespostaPorChave = new ConcurrentDictionary(); + + public Dictionary TimeoutsRespostaPorId => + _timeoutsRespostaPorId.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary TimeoutsRespostaPorDispositivo => + _timeoutsRespostaPorDispositivo.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary TimeoutsRespostaPorChave => + _timeoutsRespostaPorChave.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary PendentesAguardandoRespostaPorId + { + get + { + lock (_LockMensagens) + { + return MensagensPendentes + .Where(x => x.ComResposta && x.Enviado && !x.Respondido) + .GroupBy(x => IdKey(x.IdRx)) + .ToDictionary(g => g.Key, g => g.Count()); + } + } + } public long UltimoTxUnixMs { get; private set; } = 0; public long UltimoRxUnixMs { get; private set; } = 0; @@ -115,7 +144,7 @@ namespace AgroBase.Services public int FramesRxNaFila => FramesRecebidos.Count; - private void MostrarLog(string message, uint id) + private void MostrarLog(string message, uint id = 0) { if (DebugMode/* && new List() { 0x01 }.Contains(id)*/) { @@ -379,7 +408,7 @@ namespace AgroBase.Services } - public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true) + public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true, bool contabilizarTimeoutHealth = true, bool mensagemDescoberta = false) { if (!Connector.IsOpen) return; @@ -630,7 +659,7 @@ namespace AgroBase.Services foreach (var item in timeouts) { - TimeoutsRespostaTotal++; + RegistrarTimeoutResposta(item); MensagensPendentes.Remove(item); } @@ -652,16 +681,6 @@ namespace AgroBase.Services bool connectorOk = Connector != null && Connector.IsOpen; - if (!connectorOk) - { - IsConnected = false; - _iniciado = false; - RegistrarErroCritico("connector_waveshare_fechado"); - return; - } - - bool existeDemanda = ExisteDemandaCan(); - double ageTx = _ultimoTx == DateTime.MinValue ? 999999 : (agora - _ultimoTx).TotalSeconds; @@ -670,17 +689,41 @@ namespace AgroBase.Services ? 999999 : (agora - _ultimoRx).TotalSeconds; - if (IsConnected && existeDemanda && ageTx > 2.0 && ageRx > 2.0) + if (!connectorOk) { - RegistrarErroCritico("sem_trafego_com_demanda"); + if (_connectorIndisponivelDesde == DateTime.MinValue) + _connectorIndisponivelDesde = agora; - MostrarLog("Muito tempo sem tráfego CAN Waveshare com demanda pendente, reiniciando conexão...", 0); + double tempoSuspeito = (agora - _connectorIndisponivelDesde).TotalSeconds; + bool semTrafegoRecente = ageTx > 2.0 && ageRx > 2.0; + + if (tempoSuspeito >= 1.0 && semTrafegoRecente) + { + IsConnected = false; + _iniciado = false; + RegistrarErroCritico("connector_waveshare_fechado_confirmado"); + } + + return; + } + + _connectorIndisponivelDesde = DateTime.MinValue; + + bool existeDemanda = ExisteDemandaCan(); + + // Se existe demanda, mas TX/RX estão sem tráfego por bastante tempo, + // aí sim parece travamento real do serviço/conector. + if (IsConnected && existeDemanda && ageTx > 10.0 && ageRx > 10.0) + { + RegistrarErroCritico("sem_trafego_prolongado_com_demanda"); + + MostrarLog("Sem tráfego prolongado no CAN Waveshare com demanda, reiniciando conexão...", 0); Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog( T_Code.Can, StatusModulo.Alerta, 50, - "Muito tempo sem tráfego CAN Waveshare com demanda pendente, reiniciando conexão..." + "Sem tráfego prolongado no CAN Waveshare com demanda, reiniciando conexão..." ); ReabrirConexao(); @@ -809,7 +852,11 @@ namespace AgroBase.Services UltimoErroCritico = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency; UltimoErroCriticoUnixMs = NowUnixMs(); - MostrarLog($"Erro crítico: {motivo}", 0); + UltimoErroCriticoMotivo = motivo; + + MostrarLog($"Erro crítico: {motivo}"); + var saude = Variaveis.OperacaoEmAndamento.Sensoriamento.ModulosSaude.FirstOrDefault(x => x.modulo == T_Code.Can); + Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog(T_Code.Can, saude?.status ?? StatusModulo.Desconectado, saude?.saude ?? 0, $"Erro crítico: {motivo}"); } private void RegistrarLatenciaResposta(double latenciaMs) @@ -866,5 +913,38 @@ namespace AgroBase.Services } } + private static string IdKey(uint id) + { + return $"0x{id:X}"; + } + + private static string TimeoutChave(CanMessage msg) + { + if (msg == null) + return "desconhecido"; + + return $"{msg.Dispositivo}|tx:{IdKey(msg.IdTx)}|rx:{IdKey(msg.IdRx)}|fc_rx:0x{msg.funcCodeRx:X2}"; + } + + private void IncrementarContador(ConcurrentDictionary dict, string key) + { + dict.AddOrUpdate(key, 1, (_, atual) => atual + 1); + } + + private void RegistrarTimeoutResposta(CanMessage msg) + { + TimeoutsRespostaTotal++; + + string idRx = IdKey(msg.IdRx); + string dispositivo = msg.Dispositivo.ToString(); + string chave = TimeoutChave(msg); + + IncrementarContador(_timeoutsRespostaPorId, idRx); + IncrementarContador(_timeoutsRespostaPorDispositivo, dispositivo); + IncrementarContador(_timeoutsRespostaPorChave, chave); + + MostrarLog($"Timeout resposta CAN | {chave}", msg.IdRx); + } + } } diff --git a/AgroBase/AgroBase/Services/CanServiceEthernet.cs b/AgroBase/AgroBase/Services/CanServiceEthernet.cs index 0073919ee..46f6d8d44 100644 --- a/AgroBase/AgroBase/Services/CanServiceEthernet.cs +++ b/AgroBase/AgroBase/Services/CanServiceEthernet.cs @@ -82,7 +82,36 @@ namespace AgroBase.Services public int ErrosCriticos { get; set; } = 0; public double UltimoErroCritico { get; set; } = 0; + public string UltimoErroCriticoMotivo { get; private set; } = ""; private DateTime _ultimoErroCritico = DateTime.MinValue; + private DateTime _socketIndisponivelDesde = DateTime.MinValue; + + private readonly ConcurrentDictionary _timeoutsRespostaPorId = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _timeoutsRespostaPorDispositivo = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _timeoutsRespostaPorChave = new ConcurrentDictionary(); + + public Dictionary TimeoutsRespostaPorId => + _timeoutsRespostaPorId.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary TimeoutsRespostaPorDispositivo => + _timeoutsRespostaPorDispositivo.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary TimeoutsRespostaPorChave => + _timeoutsRespostaPorChave.ToDictionary(x => x.Key, x => x.Value); + + public Dictionary PendentesAguardandoRespostaPorId + { + get + { + lock (_LockMensagens) + { + return MensagensPendentes + .Where(x => x.ComResposta && x.Enviado && !x.Respondido) + .GroupBy(x => IdKey(x.IdRx)) + .ToDictionary(g => g.Key, g => g.Count()); + } + } + } public long UltimoTxUnixMs { get; private set; } = 0; public long UltimoRxUnixMs { get; private set; } = 0; @@ -107,6 +136,8 @@ namespace AgroBase.Services private long _txTotalUltimaJanela = 0; private long _rxTotalUltimaJanela = 0; private DateTime _ultimaJanelaFreq = DateTime.UtcNow; + private DateTime _inicioJanelaLatencia = DateTime.UtcNow; + private double _latenciaMaxJanelaMs = 0; private static long NowUnixMs() { @@ -285,7 +316,7 @@ namespace AgroBase.Services } } - public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true) + public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true, bool contabilizarTimeoutHealth = true, bool mensagemDescoberta = false) { if (!IsConnected) return; @@ -319,7 +350,9 @@ namespace AgroBase.Services funcCodeRx = funcCodeRx, DataTx = dados.ToArray(), ComResposta = get, - ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray()) + ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray()), + ContabilizarTimeoutHealth = contabilizarTimeoutHealth, + MensagemDescoberta = mensagemDescoberta }; lock (_LockMensagens) @@ -728,7 +761,7 @@ namespace AgroBase.Services foreach (var item in timeouts) { - TimeoutsRespostaTotal++; + RegistrarTimeoutResposta(item); MensagensPendentes.Remove(item); } @@ -752,15 +785,6 @@ namespace AgroBase.Services ? (_tcpClient != null && _tcpClient.Connected && _tcpStream != null) : (_udpClient != null); - if (!socketOk) - { - IsConnected = false; - RegistrarErroCritico("socket_indisponivel"); - return; - } - - bool existeDemanda = ExisteDemandaCan(); - double ageTx = _ultimoTx == DateTime.MinValue ? 999999 : (agora - _ultimoTx).TotalSeconds; @@ -769,6 +793,28 @@ namespace AgroBase.Services ? 999999 : (agora - _ultimoRx).TotalSeconds; + if (!socketOk) + { + if (_socketIndisponivelDesde == DateTime.MinValue) + _socketIndisponivelDesde = agora; + + double tempoSuspeito = (agora - _socketIndisponivelDesde).TotalSeconds; + + bool semTrafegoRecente = ageTx > 2.0 && ageRx > 2.0; + + if (tempoSuspeito >= 1.0 && semTrafegoRecente) + { + IsConnected = false; + RegistrarErroCritico("socket_indisponivel_confirmado"); + } + + return; + } + + _socketIndisponivelDesde = DateTime.MinValue; + + bool existeDemanda = ExisteDemandaCan(); + // Se existe demanda, mas houve TX recente e não houve RX, // isso parece mais "dispositivo CAN não respondeu" do que "socket morreu". // Deixa o timeout da fila e o health tratarem. @@ -912,7 +958,11 @@ namespace AgroBase.Services UltimoErroCritico = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency; UltimoErroCriticoUnixMs = NowUnixMs(); + UltimoErroCriticoMotivo = motivo; + MostrarLog($"Erro crítico: {motivo}"); + var saude = Variaveis.OperacaoEmAndamento.Sensoriamento.ModulosSaude.FirstOrDefault(x => x.modulo == T_Code.Can); + Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog(T_Code.Can, saude?.status ?? StatusModulo.Desconectado, saude?.saude ?? 0, $"Erro crítico: {motivo}"); } private void RegistrarLatenciaResposta(double latenciaMs) @@ -926,8 +976,21 @@ namespace AgroBase.Services ? latenciaMs : (alpha * latenciaMs) + ((1.0 - alpha) * LatenciaMediaMs); - if (latenciaMs > LatenciaMaxMs) - LatenciaMaxMs = latenciaMs; + var now = DateTime.UtcNow; + + if ((now - _inicioJanelaLatencia).TotalSeconds >= 10) + { + LatenciaMaxMs = _latenciaMaxJanelaMs; + _latenciaMaxJanelaMs = latenciaMs; + _inicioJanelaLatencia = now; + } + else + { + if (latenciaMs > _latenciaMaxJanelaMs) + _latenciaMaxJanelaMs = latenciaMs; + + LatenciaMaxMs = _latenciaMaxJanelaMs; + } } private void AtualizarJanelaFrequencia() @@ -969,5 +1032,59 @@ namespace AgroBase.Services } } + private static string IdKey(uint id) + { + return $"0x{id:X}"; + } + + private static string TimeoutChave(CanMessage msg) + { + if (msg == null) + return "desconhecido"; + + string chave = $"{msg.Dispositivo}|tx:{IdKey(msg.IdTx)}|rx:{IdKey(msg.IdRx)}|fc_rx:0x{msg.funcCodeRx:X2}"; + + if (new List() { T_Code.Atu, T_Code.Sen }.Contains(msg.Dispositivo) && msg.DataTx != null && msg.DataTx.Length > 1) + { + chave += $"|id_num:{msg.DataTx[1]}"; + } + + if (msg.Dispositivo != T_Code.Bat && msg.DataTx != null && msg.DataTx.Length > 0) + { + chave += $"|fc_tx:0x{msg.DataTx[0]:X2}"; + } + + return chave; + } + + private void IncrementarContador(ConcurrentDictionary dict, string key) + { + dict.AddOrUpdate(key, 1, (_, atual) => atual + 1); + } + + private void RegistrarTimeoutResposta(CanMessage msg) + { + if (msg == null) + return; + + if (!msg.ContabilizarTimeoutHealth) + { + MostrarLog($"Timeout ignorado pelo health | {TimeoutChave(msg)}", msg.IdRx); + return; + } + + TimeoutsRespostaTotal++; + + string idRx = IdKey(msg.IdRx); + string dispositivo = msg.Dispositivo.ToString(); + string chave = TimeoutChave(msg); + + IncrementarContador(_timeoutsRespostaPorId, idRx); + IncrementarContador(_timeoutsRespostaPorDispositivo, dispositivo); + IncrementarContador(_timeoutsRespostaPorChave, chave); + + MostrarLog($"Timeout resposta CAN | {chave}", msg.IdRx); + } + } } diff --git a/AgroBase/AgroBase/Services/DalyBMSService.cs b/AgroBase/AgroBase/Services/DalyBMSService.cs index 5a5d07b98..774fe7fcb 100644 --- a/AgroBase/AgroBase/Services/DalyBMSService.cs +++ b/AgroBase/AgroBase/Services/DalyBMSService.cs @@ -33,7 +33,7 @@ namespace AgroBase.Services CanManager.CanService.RegistrarHandler(CanManager.ConverterIdCan(MakeResponseID(f)), _BatHandler); } - RequisitarDado(BatFuncCode.SOC); + EnviarComandoDescoberta(); DateTime Inicio = DateTime.Now; bool Respondido() => DadosLeitura.UltimoComandoRecebido > Inicio; @@ -94,6 +94,12 @@ namespace AgroBase.Services #region Helpers de protocolo (ID, DataID, etc.) + private static void EnviarComandoDescoberta() + { + BatFuncCode dado = BatFuncCode.SOC; + CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, MakeRequestID(dado), MakeResponseID(dado), (byte)dado, (byte)dado, payload: new byte[8], get: true, contabilizarTimeoutHealth: false, mensagemDescoberta: true); + } + private static void EnviarComando(T_Code Dispositivo, byte[] idTx, byte[] idRx, byte funcCode, byte[] payload = null) { CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, idTx, idRx, funcCode, funcCode, payload, get: true); diff --git a/AgroBase/AgroBase/Services/DispositivosService.cs b/AgroBase/AgroBase/Services/DispositivosService.cs index 89fbcba95..4ea0ba9a3 100644 --- a/AgroBase/AgroBase/Services/DispositivosService.cs +++ b/AgroBase/AgroBase/Services/DispositivosService.cs @@ -64,7 +64,7 @@ namespace AgroBase.Comum VariaveisOperacao.RegistrarLogDispositivo(Logs, Dispositivo, ".json", 100); } - public void AdicionarMensagemFila(int ID_Num, CanMessagePosicaoDados PosicaoTx, CanMessagePosicaoDados PosicaoRx, byte[] dados = null, bool get = true) + public void AdicionarMensagemFila(int ID_Num, CanMessagePosicaoDados PosicaoTx, CanMessagePosicaoDados PosicaoRx, byte[] dados = null, bool get = true, bool contabilizarTimeoutHealth = true, bool mensagemDescoberta = false) { List payload = new List() { @@ -74,7 +74,7 @@ namespace AgroBase.Comum { payload.AddRange(dados); } - CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { Dados._EnderecoCAN_Tx }, new byte[] { Dados._EnderecoCAN_Rx }, (byte)PosicaoTx, (byte)PosicaoRx, payload.ToArray(), get); + CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { Dados._EnderecoCAN_Tx }, new byte[] { Dados._EnderecoCAN_Rx }, (byte)PosicaoTx, (byte)PosicaoRx, payload.ToArray(), get, contabilizarTimeoutHealth, mensagemDescoberta); } public Control InstanciarDispositivo(int index) diff --git a/AgroBase/AgroBase/Services/IDispositivosService.cs b/AgroBase/AgroBase/Services/IDispositivosService.cs index fefb8f5a7..288fc6bb6 100644 --- a/AgroBase/AgroBase/Services/IDispositivosService.cs +++ b/AgroBase/AgroBase/Services/IDispositivosService.cs @@ -14,7 +14,7 @@ namespace AgroBase.Services Control InstanciarDispositivo(int index); void AtualizaIndex(int index); void SalvarParametros(DispositivoBaseModel dados); - void AdicionarMensagemFila(int ID_Num, CanMessagePosicaoDados PosicaoTx, CanMessagePosicaoDados PosicaoRx, byte[] dados = null, bool get = true); + void AdicionarMensagemFila(int ID_Num, CanMessagePosicaoDados PosicaoTx, CanMessagePosicaoDados PosicaoRx, byte[] dados = null, bool get = true, bool contabilizarTimeoutHealth = true, bool mensagemDescoberta = false); void EnviarProtocolosConfiguracao(bool Conectar = true); void CarregarParametrosModulos(); } diff --git a/AgroBase/AgroBase/Services/MKS057DCanService.cs b/AgroBase/AgroBase/Services/MKS057DCanService.cs index 9afb8a099..3106ff6d5 100644 --- a/AgroBase/AgroBase/Services/MKS057DCanService.cs +++ b/AgroBase/AgroBase/Services/MKS057DCanService.cs @@ -50,7 +50,7 @@ namespace AgroBase.Services { CanManager.CanService.RegistrarHandler(Modulo.DirMotor._EnderecoCAN_Rx, _MksHandler); - RequisitarDado(Modulo.DirMotor._EnderecoCAN_Tx, Modulo.DirMotor._EnderecoCAN_Rx, MksFuncCode.Versao); + EnviarComandoDescoberta(Modulo.DirMotor._EnderecoCAN_Tx, Modulo.DirMotor._EnderecoCAN_Rx); DateTime Inicio = DateTime.Now; bool Respondido() => DadosLeitura.Any(x => x.EnderecoRx == Modulo.DirMotor._EnderecoCAN_Rx && x.UltimoComandoRecebido > Inicio); @@ -124,6 +124,11 @@ namespace AgroBase.Services #region COMANDOS + private static void EnviarComandoDescoberta(byte idTx, byte idRx) + { + CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { idTx }, new byte[] { idRx }, (byte)MksFuncCode.Versao, (byte)MksFuncCode.Versao, null, true, false, true); + } + private static void EnviarComando(T_Code Dispositivo, byte idTx, byte idRx, byte funcCode, byte[] payload = null, bool get = true) { CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { idTx }, new byte[] { idRx }, funcCode, funcCode, payload, get); diff --git a/AgroBase/AgroBase/Services/OIDCanService.cs b/AgroBase/AgroBase/Services/OIDCanService.cs index b1e72bf45..1430db6a1 100644 --- a/AgroBase/AgroBase/Services/OIDCanService.cs +++ b/AgroBase/AgroBase/Services/OIDCanService.cs @@ -43,7 +43,7 @@ namespace AgroBase.Services { CanManager.CanService.RegistrarHandler(Modulo.MovMotor._EnderecoCAN_Rx, _OidHandler); - RequisitarDado(Modulo.MovMotor._EnderecoCAN_Tx, Modulo.MovMotor._EnderecoCAN_Rx, OidParametros.CodErro); + EnviarComandoDescoberta(Modulo.MovMotor._EnderecoCAN_Tx, Modulo.MovMotor._EnderecoCAN_Rx); DateTime Inicio = DateTime.Now; bool Respondido() => DadosLeitura.Any(x => x.EnderecoRx == Modulo.MovMotor._EnderecoCAN_Rx && x.UltimoComandoRecebido > Inicio); @@ -107,6 +107,11 @@ namespace AgroBase.Services #region COMANDOS + private static void EnviarComandoDescoberta(byte idTx, byte idRx) + { + CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { idTx }, new byte[] { idRx }, (byte)OidParametros.CodErro, (byte)OidParametros.CodErro, null, true, false, true); + } + private static void EnviarComando(T_Code Dispositivo, byte idTx, byte idRx, byte funcCode, byte[] payload = null, bool get = true) { CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { idTx }, new byte[] { idRx }, funcCode, funcCode, payload, get); diff --git a/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs b/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs index bf2e904ed..b464bede1 100644 --- a/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs +++ b/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs @@ -872,6 +872,12 @@ namespace AgroBase.Services.Operadores ("timeouts_resposta_total", DadosCan.TimeoutsRespostaTotal), ("reconexoes_total", DadosCan.ReconexoesTotal), + // Timeouts por origem + ("timeouts_por_id_total", DadosCan.TimeoutsRespostaPorId), + ("timeouts_por_dispositivo_total", DadosCan.TimeoutsRespostaPorDispositivo), + ("timeouts_por_chave_total", DadosCan.TimeoutsRespostaPorChave), + ("pendentes_resp_por_id", DadosCan.PendentesAguardandoRespostaPorId), + // Filas ("pendentes_total", DadosCan.PendentesTotal), ("pendentes_nao_enviadas", DadosCan.PendentesNaoEnviadas), @@ -886,7 +892,8 @@ namespace AgroBase.Services.Operadores ("erros_criticos", DadosCan.ErrosCriticos), ("last_tx", DadosCan.UltimoTx), ("last_rx", DadosCan.UltimoRx), - ("last_ec", DadosCan.UltimoErroCritico) + ("last_ec", DadosCan.UltimoErroCritico), + ("last_ec_motivo", DadosCan.UltimoErroCriticoMotivo) ); double MinOrDefault(List items) diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/can.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/can.py index bd83a867d..3cdfd47e5 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/can.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/can.py @@ -13,6 +13,35 @@ class ModuloCAN(ModuloDiagnosticoBase): self._last_tx_fail = None self._last_rx_invalid = None self._last_sample_ts = None + self._last_reconexoes = None + + self._last_timeouts_por_id = {} + self._last_timeouts_por_dispositivo = {} + self._last_timeouts_por_chave = {} + + self._saude_suavizada = None + self._status_atual = StatusModulo.OPERANTE + self._ciclos_alerta = 0 + self._ciclos_ok = 0 + + def _delta_dict(self, atual, anterior): + if not isinstance(atual, dict): + return {} + + anterior = anterior or {} + deltas = {} + + for k, v in atual.items(): + try: + atual_v = int(v or 0) + anterior_v = int(anterior.get(k, 0) or 0) + d = max(0, atual_v - anterior_v) + if d > 0: + deltas[k] = d + except: + continue + + return deltas def _delta(self, atual, anterior): if anterior is None: @@ -32,6 +61,11 @@ class ModuloCAN(ModuloDiagnosticoBase): last_rx_ms = float(m.get("last_rx_ms", 0.0) or 0.0) last_ec_ms = float(m.get("last_ec_ms", 0.0) or 0.0) + last_ec_motivo = m.get("last_ec_motivo", "") or "" + reconexoes_total = int(m.get("reconexoes_total", 0) or 0) + delta_reconexoes = self._delta(reconexoes_total, self._last_reconexoes) + self._last_reconexoes = reconexoes_total + age_tx = ((now_ms - last_tx_ms) / 1000.0) if last_tx_ms > 0 else 999999.0 age_rx = ((now_ms - last_rx_ms) / 1000.0) if last_rx_ms > 0 else 999999.0 age_ec = ((now_ms - last_ec_ms) / 1000.0) if last_ec_ms > 0 else 999999.0 @@ -45,6 +79,10 @@ class ModuloCAN(ModuloDiagnosticoBase): frames_rx_fila = int(m.get("frames_rx_fila", 0) or 0) timeouts_total = int(m.get("timeouts_resposta_total", 0) or 0) + timeouts_por_id_total = m.get("timeouts_por_id_total", {}) or {} + timeouts_por_dispositivo_total = m.get("timeouts_por_dispositivo_total", {}) or {} + timeouts_por_chave_total = m.get("timeouts_por_chave_total", {}) or {} + pendentes_resp_por_id = m.get("pendentes_resp_por_id", {}) or {} tx_falha_total = int(m.get("tx_falha_total", 0) or 0) rx_invalid_total = int(m.get("rx_invalido_total", 0) or 0) @@ -57,6 +95,25 @@ class ModuloCAN(ModuloDiagnosticoBase): delta_tx_fail = self._delta(tx_falha_total, self._last_tx_fail) delta_rx_invalid = self._delta(rx_invalid_total, self._last_rx_invalid) + delta_timeouts_por_id = self._delta_dict( + timeouts_por_id_total, + self._last_timeouts_por_id + ) + + delta_timeouts_por_dispositivo = self._delta_dict( + timeouts_por_dispositivo_total, + self._last_timeouts_por_dispositivo + ) + + delta_timeouts_por_chave = self._delta_dict( + timeouts_por_chave_total, + self._last_timeouts_por_chave + ) + + self._last_timeouts_por_id = dict(timeouts_por_id_total) + self._last_timeouts_por_dispositivo = dict(timeouts_por_dispositivo_total) + self._last_timeouts_por_chave = dict(timeouts_por_chave_total) + self._last_timeouts = timeouts_total self._last_tx_fail = tx_falha_total self._last_rx_invalid = rx_invalid_total @@ -96,6 +153,16 @@ class ModuloCAN(ModuloDiagnosticoBase): saude -= p motivos.append(f"{delta_timeouts} timeout(s) de resposta no ciclo (-{p}%)") + if delta_timeouts_por_id: + top_ids = sorted( + delta_timeouts_por_id.items(), + key=lambda x: x[1], + reverse=True + )[:3] + + texto_ids = ", ".join([f"{k}: +{v}" for k, v in top_ids]) + motivos.append(f"timeouts por ID no ciclo: {texto_ids}") + if delta_tx_fail > 0: p = min(30, delta_tx_fail * 10) saude -= p @@ -120,13 +187,15 @@ class ModuloCAN(ModuloDiagnosticoBase): # Latência. if lat_media > 150: p = min(25, int((lat_media - 150) / 20)) - saude -= p - motivos.append(f"latência média alta: {lat_media:.0f}ms (-{p}%)") + if p > 0: + saude -= p + motivos.append(f"latência média alta: {lat_media:.0f}ms (-{p}%)") if lat_max > 500: p = min(15, int((lat_max - 500) / 100) + 5) - saude -= p - motivos.append(f"pico de latência: {lat_max:.0f}ms (-{p}%)") + if p > 0: + saude -= p + motivos.append(f"pico de latência: {lat_max:.0f}ms (-{p}%)") # Erro crítico recente, com decaimento. if erros_criticos > 0 and age_ec <= 600: @@ -134,25 +203,69 @@ class ModuloCAN(ModuloDiagnosticoBase): p = int(round(min(erros_criticos * 10, 40) * fator)) if p > 0: saude -= p - motivos.append(f"{erros_criticos} erro(s) crítico(s), último há {age_ec:.0f}s (-{p}%)") + motivo_ec = f"{erros_criticos} erro(s) crítico(s)" + if last_ec_motivo: + motivo_ec += f" ({last_ec_motivo})" + motivo_ec += f", último há {age_ec:.0f}s (-{p}%)" + motivos.append(motivo_ec) saude = max(0, min(100, saude)) - status = StatusModulo.OPERANTE + if delta_reconexoes > 0: + motivos.append(f"{delta_reconexoes} reconexão(ões) CAN no ciclo") - if not conectado: - status = StatusModulo.DESCONECTADO - elif saude <= 20: - status = StatusModulo.FALHA - elif saude < 80: - status = StatusModulo.ALERTA + saude_instantanea = max(0, min(100, saude)) + if self._saude_suavizada is None: + self._saude_suavizada = saude_instantanea + else: + alpha = 0.35 + self._saude_suavizada = ( + alpha * saude_instantanea + + (1.0 - alpha) * self._saude_suavizada + ) + + saude = int(round(self._saude_suavizada)) + status = self._atualizar_status_suavizado(conectado, saude) + + saude_individual = [] + for id_can, delta in sorted(delta_timeouts_por_id.items(), key=lambda x: x[1], reverse=True)[:10]: + total_id = int(timeouts_por_id_total.get(id_can, 0) or 0) + pend_id = int(pendentes_resp_por_id.get(id_can, 0) or 0) + + p_id = min(80, delta * 25) + saude_id = max(0, 100 - p_id) + + if saude_id <= 20: + status_id = StatusModulo.FALHA + elif saude_id < 80: + status_id = StatusModulo.ALERTA + else: + status_id = StatusModulo.OPERANTE + + motivos_id = [ + f"{delta} timeout(s) recente(s)", + f"{total_id} timeout(s) acumulado(s)" + ] + + if pend_id > 0: + motivos_id.append(f"{pend_id} resposta(s) pendente(s)") + + saude_individual.append({ + "id": id_can, + "label": id_can, + "status": status_id.value, + "saude": saude_id, + "motivos": motivos_id, + "condicoes_operacionais": [], + }) payload = { "conectado": conectado, "status": status.value, "saude": saude, + "saude_instantanea": saude_instantanea, "motivos": motivos, - "saude_individual": [], + "saude_individual": saude_individual, "condicoes_operacionais": [], "detalhes": { "age_rx_s": age_rx, @@ -165,10 +278,21 @@ class ModuloCAN(ModuloDiagnosticoBase): "pendentes_aguardando_resposta": pend_resp, "frames_rx_fila": frames_rx_fila, "timeouts_resposta_total": timeouts_total, + "timeouts_por_id_total": timeouts_por_id_total, + "timeouts_por_dispositivo_total": timeouts_por_dispositivo_total, + "timeouts_por_chave_total": timeouts_por_chave_total, + "delta_timeouts_por_id": delta_timeouts_por_id, + "delta_timeouts_por_dispositivo": delta_timeouts_por_dispositivo, + "delta_timeouts_por_chave": delta_timeouts_por_chave, + "pendentes_resp_por_id": pendentes_resp_por_id, "tx_falha_total": tx_falha_total, "rx_invalido_total": rx_invalid_total, "latencia_media_ms": lat_media, "latencia_max_ms": lat_max, + "erros_criticos": erros_criticos, + "last_ec_motivo": last_ec_motivo, + "reconexoes_total": reconexoes_total, + "delta_reconexoes": delta_reconexoes, }, } @@ -178,4 +302,38 @@ class ModuloCAN(ModuloDiagnosticoBase): ) except Exception as e: - print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}") \ No newline at end of file + print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}") + + def _atualizar_status_suavizado(self, conectado, saude): + if not conectado: + self._status_atual = StatusModulo.DESCONECTADO + self._ciclos_alerta = 0 + self._ciclos_ok = 0 + return self._status_atual + + if saude <= 20: + self._status_atual = StatusModulo.FALHA + self._ciclos_alerta = 0 + self._ciclos_ok = 0 + return self._status_atual + + if saude < 80: + self._ciclos_alerta += 1 + self._ciclos_ok = 0 + elif saude >= 85: + self._ciclos_ok += 1 + self._ciclos_alerta = 0 + else: + # Zona neutra: não troca status. + self._ciclos_alerta = 0 + self._ciclos_ok = 0 + + # Entra em alerta só depois de 2 ciclos ruins. + if self._ciclos_alerta >= 2: + self._status_atual = StatusModulo.ALERTA + + # Volta para operante só depois de 3 ciclos bons. + if self._ciclos_ok >= 3: + self._status_atual = StatusModulo.OPERANTE + + return self._status_atual \ No newline at end of file