revisado conexão can

This commit is contained in:
Diego Freitas 2026-06-11 19:40:44 -03:00
parent 1a170c10b3
commit 3b15a2473c
17 changed files with 604 additions and 109 deletions

View File

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

View File

@ -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");

View File

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

View File

@ -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);
}

View File

@ -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);
}

View File

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

View File

@ -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<T>
@ -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<string, long> TimeoutsRespostaPorId { get; }
Dictionary<string, long> TimeoutsRespostaPorDispositivo { get; }
Dictionary<string, long> TimeoutsRespostaPorChave { get; }
Dictionary<string, int> PendentesAguardandoRespostaPorId { get; }
long TxTotal { get; }
long RxTotal { get; }
long TxFalhaTotal { get; }

View File

@ -32,7 +32,7 @@ namespace AgroBase.Services
private readonly ConcurrentDictionary<uint, bool> _roteadoresRegistrados = new ConcurrentDictionary<uint, bool>();
private uint? _ultimoIdEnviado = null;
private List<CanMessage> Mensagens = new List<CanMessage>();
private List<CanMessage> MensagensPendentes = new List<CanMessage>();
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<string, long> _timeoutsRespostaPorId = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, long> _timeoutsRespostaPorDispositivo = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, long> _timeoutsRespostaPorChave = new ConcurrentDictionary<string, long>();
public Dictionary<string, long> TimeoutsRespostaPorId =>
_timeoutsRespostaPorId.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, long> TimeoutsRespostaPorDispositivo =>
_timeoutsRespostaPorDispositivo.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, long> TimeoutsRespostaPorChave =>
_timeoutsRespostaPorChave.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, int> 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<uint>() { 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<string, long> 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);
}
}
}

View File

@ -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<string, long> _timeoutsRespostaPorId = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, long> _timeoutsRespostaPorDispositivo = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, long> _timeoutsRespostaPorChave = new ConcurrentDictionary<string, long>();
public Dictionary<string, long> TimeoutsRespostaPorId =>
_timeoutsRespostaPorId.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, long> TimeoutsRespostaPorDispositivo =>
_timeoutsRespostaPorDispositivo.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, long> TimeoutsRespostaPorChave =>
_timeoutsRespostaPorChave.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, int> 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<uint>() { 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<string, long> 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);
}
}
}

View File

@ -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<string, long> _timeoutsRespostaPorId = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, long> _timeoutsRespostaPorDispositivo = new ConcurrentDictionary<string, long>();
private readonly ConcurrentDictionary<string, long> _timeoutsRespostaPorChave = new ConcurrentDictionary<string, long>();
public Dictionary<string, long> TimeoutsRespostaPorId =>
_timeoutsRespostaPorId.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, long> TimeoutsRespostaPorDispositivo =>
_timeoutsRespostaPorDispositivo.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, long> TimeoutsRespostaPorChave =>
_timeoutsRespostaPorChave.ToDictionary(x => x.Key, x => x.Value);
public Dictionary<string, int> 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>() { 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<string, long> 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);
}
}
}

View File

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

View File

@ -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<byte> payload = new List<byte>()
{
@ -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)

View File

@ -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();
}

View File

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

View File

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

View File

@ -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<double> items)

View File

@ -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}")
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