saude do atuador, telemetria recente sensoriamento, robustez atuador, robustez sensoriamento e diagnostico i2c, ciclo de vida do can service ethernet, firmware sensoriamento com barramento i2c melhor gerenciado

This commit is contained in:
Diego Freitas 2026-09-03 21:29:15 -03:00
parent 8054055ec8
commit 9410933079
16 changed files with 5461 additions and 1873 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4699,6 +4699,7 @@ namespace AgroBase.Models
model.RequisitarDados = dados.RequisitarDados;
model.LatenciaLoopMs = leitura?.LatenciaLoop ?? 0;
model.StatusI2C = new Dictionary<CanMessagePosicaoDados, byte[]>(leitura?.StatusI2C ?? new Dictionary<CanMessagePosicaoDados, byte[]>());
model.UltimoComandoRespondido = leitura?.UltimoComandoRespondido ?? DateTime.MinValue;
model.IdadeUltimoComandoMs = IdadeMs(model.UltimoComandoRespondido);
@ -6132,6 +6133,7 @@ namespace AgroBase.Models
public bool RequisitarDados { get; set; }
public int LatenciaLoopMs { get; set; }
public Dictionary<CanMessagePosicaoDados, byte[]> StatusI2C { get; set; }
public DateTime UltimoComandoRespondido { get; set; } = DateTime.MinValue;
public double IdadeUltimoComandoMs { get; set; }
@ -6196,6 +6198,7 @@ namespace AgroBase.Models
RequisitarDados = RequisitarDados,
LatenciaLoopMs = LatenciaLoopMs,
StatusI2C = new Dictionary<CanMessagePosicaoDados, byte[]>(StatusI2C ?? new Dictionary<CanMessagePosicaoDados, byte[]>()),
UltimoComandoRespondido = UltimoComandoRespondido,
IdadeUltimoComandoMs = IdadeUltimoComandoMs,

View File

@ -315,8 +315,7 @@ namespace AgroBase.Services
catch (Exception ex)
{
MostrarLog($"Erro ao inicializar: {ex.Message}");
IsConnected = false;
Iniciado = false;
MarcarTransporteIndisponivel();
return false;
}
}
@ -326,23 +325,41 @@ namespace AgroBase.Services
try
{
_tcpClient = new TcpClient();
_tcpClient.NoDelay = true;
_tcpClient.ReceiveBufferSize = 64 * 1024;
_tcpClient.SendBufferSize = 64 * 1024;
var task = _tcpClient.ConnectAsync(IpAddress, Port);
if (!task.Wait(1200))
return false;
if (!_tcpClient.Connected)
if (!task.Wait(1200) || !_tcpClient.Connected)
{
try { _tcpClient.Close(); }
catch { }
_tcpClient = null;
_tcpStream = null;
return false;
}
_tcpStream = _tcpClient.GetStream();
return true;
}
catch (Exception ex)
{
try { _tcpStream?.Close(); }
catch { }
try { _tcpClient?.Close(); }
catch { }
_tcpStream = null;
_tcpClient = null;
MostrarLog($"Erro TCP: {ex.Message}");
return false;
}
}
@ -368,18 +385,15 @@ namespace AgroBase.Services
private void DefinirDispositivo()
{
if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Can))
if (!IsConnected || !Iniciado)
return;
SerialService.AdicionarOuAtualizarDispositivoMapeado(new DispositivoDetalhesModel
{
if (IsConnected && Iniciado)
{
SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel()
{
Dispositivo = T_Code.Can,
Endereco = IpAddress,
Versao = ("1").ToString()
});
}
}
Dispositivo = T_Code.Can,
Endereco = IpAddress,
Versao = "1"
});
}
public bool AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true, bool contabilizarTimeoutHealth = true, bool mensagemDescoberta = false)
@ -599,7 +613,7 @@ namespace AgroBase.Services
if (!EnviarPacote(packet, msg.IdTx))
{
MostrarLog($"Erro ao enviar frame ID=0x{msg.IdTx:X}", msg.IdTx);
IsConnected = false;
MarcarTransporteIndisponivel();
break;
}
@ -695,7 +709,7 @@ namespace AgroBase.Services
catch (Exception ex)
{
MostrarLog($"Erro RX: {ex.Message}");
IsConnected = false;
MarcarTransporteIndisponivel();
}
await Task.CompletedTask;
@ -703,54 +717,71 @@ namespace AgroBase.Services
private void OuvirTcp()
{
if (_tcpClient == null || _tcpStream == null || !_tcpClient.Connected)
lock (_socketLock)
{
IsConnected = false;
return;
}
if (_tcpClient == null || _tcpStream == null || !_tcpClient.Connected)
{
MarcarTransporteIndisponivel();
return;
}
while (_tcpStream.DataAvailable)
{
byte[] buffer = new byte[4096];
int lidos = _tcpStream.Read(buffer, 0, buffer.Length);
if (lidos <= 0)
break;
while (_tcpStream.DataAvailable)
{
byte[] buffer = new byte[4096];
for (int i = 0; i < lidos; i++)
TcpRxBuffer.Add(buffer[i]);
}
int lidos = _tcpStream.Read(buffer, 0, buffer.Length);
while (TcpRxBuffer.Count >= USR_FRAME_SIZE)
{
byte[] packet = TcpRxBuffer.Take(USR_FRAME_SIZE).ToArray();
TcpRxBuffer.RemoveRange(0, USR_FRAME_SIZE);
ProcessarPacoteUsrStandard(packet);
if (lidos <= 0)
break;
for (int i = 0; i < lidos; i++)
TcpRxBuffer.Add(buffer[i]);
}
while (TcpRxBuffer.Count >= USR_FRAME_SIZE)
{
byte[] packet = TcpRxBuffer.Take(USR_FRAME_SIZE).ToArray();
TcpRxBuffer.RemoveRange(0, USR_FRAME_SIZE);
ProcessarPacoteUsrStandard(packet);
}
}
}
private void OuvirUdp()
{
if (_udpClient == null)
lock (_socketLock)
{
IsConnected = false;
return;
}
while (_udpClient.Available > 0)
{
IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
byte[] pacote = _udpClient.Receive(ref ep);
if (pacote == null || pacote.Length == 0)
continue;
int offset = 0;
while (offset + USR_FRAME_SIZE <= pacote.Length)
if (_udpClient == null)
{
byte[] frame = new byte[USR_FRAME_SIZE];
Buffer.BlockCopy(pacote, offset, frame, 0, USR_FRAME_SIZE);
ProcessarPacoteUsrStandard(frame);
offset += USR_FRAME_SIZE;
MarcarTransporteIndisponivel();
return;
}
while (_udpClient.Available > 0)
{
IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
byte[] pacote = _udpClient.Receive(ref ep);
if (pacote == null || pacote.Length == 0)
{
continue;
}
int offset = 0;
while (offset + USR_FRAME_SIZE <= pacote.Length)
{
byte[] frame = new byte[USR_FRAME_SIZE];
Buffer.BlockCopy(pacote, offset, frame, 0, USR_FRAME_SIZE);
ProcessarPacoteUsrStandard(frame);
offset += USR_FRAME_SIZE;
}
}
}
}
@ -1142,8 +1173,8 @@ namespace AgroBase.Services
DateTime agora = DateTime.Now;
bool socketOk = TransportMode == CanEthernetTransportMode.Tcp
? (_tcpClient != null && _tcpClient.Connected && _tcpStream != null)
: (_udpClient != null);
? TcpSocketSaudavel()
: _udpClient != null;
double ageTx = _ultimoTx == DateTime.MinValue
? 999999
@ -1159,13 +1190,11 @@ namespace AgroBase.Services
_socketIndisponivelDesde = agora;
double tempoSuspeito = (agora - _socketIndisponivelDesde).TotalSeconds;
bool semTrafegoRecente = ageTx > 2.0 && ageRx > 2.0;
if (tempoSuspeito >= 1.0 && semTrafegoRecente)
if (tempoSuspeito >= 1.0 && (IsConnected || Iniciado))
{
IsConnected = false;
MarcarTransporteIndisponivel();
RegistrarErroCritico("socket_indisponivel_confirmado");
MostrarLog("Socket CAN-ETH indisponível; aguardando reinicialização do transporte...");
}
return;
@ -1174,25 +1203,50 @@ namespace AgroBase.Services
_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.
if (IsConnected && existeDemanda && ageTx > 10.0 && ageRx > 10.0)
bool txAtivo = ageTx <= 2.0 || FreqTxJanela > 0.5;
if (IsConnected && existeDemanda && txAtivo && ageRx > 8.0)
{
RegistrarErroCritico("sem_trafego_prolongado_com_demanda");
MostrarLog("Sem tráfego prolongado no CAN-ETH com demanda, reiniciando conexão...");
RegistrarErroCritico("tx_ativo_sem_rx_prolongado");
MostrarLog("TX ativo sem RX prolongado; reiniciando conexão CAN-ETH...");
ReabrirConexao();
}
if (_ultimoErroCritico != DateTime.MinValue &&
(agora - _ultimoErroCritico).TotalMinutes > 10)
if (_ultimoErroCritico != DateTime.MinValue && (agora - _ultimoErroCritico).TotalMinutes > 10)
{
ErrosCriticos = 0;
}
}
private void ReabrirConexao()
private bool TcpSocketSaudavel()
{
try
{
if (_tcpClient == null ||
_tcpClient.Client == null ||
_tcpStream == null ||
!_tcpClient.Connected)
{
return false;
}
Socket socket = _tcpClient.Client;
bool remotoFechou =
socket.Poll(
0,
SelectMode.SelectRead
) &&
socket.Available == 0;
return !remotoFechou;
}
catch
{
return false;
}
}
private bool ReabrirConexao()
{
try
{
@ -1200,15 +1254,23 @@ namespace AgroBase.Services
FecharConexaoSemLimparHandlers();
if (TransportMode == CanEthernetTransportMode.Tcp)
IsConnected = ConectarTcp();
else
IsConnected = ConectarUdp();
bool conectado = TransportMode == CanEthernetTransportMode.Tcp
? ConectarTcp()
: ConectarUdp();
Iniciado = IsConnected;
IsConnected = conectado;
Iniciado = conectado;
if (!conectado)
{
MostrarLog($"Reconexão CAN-ETH falhou em {IpAddress}:{Port}");
return false;
}
ReconexoesTotal++;
_socketIndisponivelDesde = DateTime.MinValue;
_ultimoRx = DateTime.Now;
_ultimoTx = DateTime.Now;
@ -1217,11 +1279,16 @@ namespace AgroBase.Services
UltimoRxUnixMs = NowUnixMs();
UltimoTxUnixMs = NowUnixMs();
MostrarLog($"CAN-ETH reconectado em {IpAddress}:{Port}");
return true;
}
catch
catch (Exception ex)
{
IsConnected = false;
Iniciado = false;
MarcarTransporteIndisponivel();
MostrarLog($"Erro ao reabrir CAN-ETH: {ex.Message}");
return false;
}
}
@ -1260,6 +1327,12 @@ namespace AgroBase.Services
Iniciado = false;
}
private void MarcarTransporteIndisponivel()
{
IsConnected = false;
Iniciado = false;
}
public void Fechar()
{
tmrOuvirCAN?.Dispose();

View File

@ -445,7 +445,7 @@ namespace AgroBase.Services
if (leitura == null)
continue;
SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel()
SerialService.AdicionarOuAtualizarDispositivoMapeado(new DispositivoDetalhesModel()
{
Dispositivo = T_Code.Dir,
Endereco = CanManager.CanService._portName,

View File

@ -456,7 +456,7 @@ namespace AgroBase.Services
if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Mov && x.Mod_ID == modId))
{
SerialService.DispositivosMapeados.Add(
SerialService.AdicionarOuAtualizarDispositivoMapeado(
new DispositivoDetalhesModel
{
Dispositivo = T_Code.Mov,

View File

@ -164,7 +164,7 @@ namespace AgroBase.Services.Operadores
double pressaoAlvo = bombaLinha?._PressaoSPControle > 0 ? bombaLinha._PressaoSPControle : op.Parametros?.Controle?.AtuPressaoLinha ?? 0;
double pressaoLinha = bombaLinha?._PressaoAtual ?? _Sensoriamento?.Atuador?.PressaoLinha ?? 0;
double toleranciaPressao = Math.Max(2.0, pressaoAlvo * 0.15);
bool pressaoLinhaRegular = pressaoAlvo <= 0 || Math.Abs(pressaoLinha - pressaoAlvo) <= toleranciaPressao;
bool pressaoLinhaRegular = pressaoAlvo <= 0 || pressaoLinha >= Math.Max(0, pressaoAlvo - toleranciaPressao);
RedisService.AtualizarCampos(
CtxKey.DadosContexto,
@ -222,7 +222,7 @@ namespace AgroBase.Services.Operadores
("Atuador", new
{
pressao_linha_regular = pressaoLinhaRegular,
pressao_linha = _Sensoriamento.Atuador?.PressaoLinha ?? 0,
pressao_linha = pressaoLinha,
potencia_bomba = _Sensoriamento.Atuador?.LeituraPotenciaBomba ?? 0,
estado_bomba = _Sensoriamento.Atuador?.LeituraComandoBomba ?? false,
tempo_bomba_atuada = _Sensoriamento.Atuador?.TempoBombaAtuada ?? 0,
@ -537,6 +537,17 @@ namespace AgroBase.Services.Operadores
var agora_dt = DateTime.Now;
var agora = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
double CalcularIdadeMs(DateTime instante)
{
if (instante == DateTime.MinValue)
return 999999;
return Math.Max(
0,
(agora_dt - instante).TotalMilliseconds
);
}
//Console.WriteLine(RedisService.Get(RedisService.ModKey(Enums.T_Code.Gps)));
//Console.WriteLine(RedisService.Get(RedisService.ModKey(Enums.T_Code.Dir)));
//Console.WriteLine(RedisService.Get(RedisService.ModKey(Enums.T_Code.Mov)));
@ -547,6 +558,8 @@ namespace AgroBase.Services.Operadores
//RedisService.SetObject(RedisService.ModKey(Enums.T_Code.Mov), string.Empty);
//RedisService.SetObject(RedisService.ModKey(Enums.T_Code.Sen), string.Empty);
#region MOV e DIR
var dirAtivos = new List<int>();
var movAtivos = new List<int>();
@ -700,18 +713,20 @@ namespace AgroBase.Services.Operadores
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Dir), ("ativos", dirAtivos.ToList()));
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Mov), ("ativos", movAtivos.ToList()));
#endregion
#region SEN
var DadosSen = op.DispSen.Dados;
DateTime ultimaRespostaSen = DadosSen.DadosLeitura?.UltimoComandoRespondido ?? DateTime.MinValue;
double idadeUltimaRespostaSenMs =
ultimaRespostaSen == DateTime.MinValue
? 999999
: (agora_dt - ultimaRespostaSen).TotalMilliseconds;
double idadeUltimaRespostaSenMs = CalcularIdadeMs(ultimaRespostaSen);
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Sen),
("timestamp", agora),
("conectado", DadosSen.Conectado),
("telemetria_recente", DadosSen.TelemetriaRecente),
("firmware_conectado", DadosSen.FirmwareDeclarouConectado),
("freq_base", DadosSen.FrequenciaBase),
("bms_ligado", _Sensoriamento.Bateria?.BmsLigado ?? false),
@ -743,10 +758,7 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimaRespostaSensorMs =
sensor.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - sensor.UltimaLeitura).TotalMilliseconds;
double idadeUltimaRespostaSensorMs = CalcularIdadeMs(sensor.UltimaLeitura);
var sensorObj = new
{
@ -754,6 +766,7 @@ namespace AgroBase.Services.Operadores
id,
label = sensor.ID,
tipo,
freq_base = DadosSen.FrequenciaBase,
aferir = sensor.Aferir,
mandatorio = sensor.Mandatorio,
dados,
@ -799,15 +812,8 @@ namespace AgroBase.Services.Operadores
bool possuiRespostaServo = ultimaRespostaAnguloServo != DateTime.MinValue;
double idadeUltimoComandoServoMs =
!possuiComandoServo
? 999999
: Math.Max(0, (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds);
double idadeUltimaRespostaServoMs =
!possuiRespostaServo
? 999999
: Math.Max(0, (agora_dt - ultimaRespostaAnguloServo).TotalMilliseconds);
double idadeUltimoComandoServoMs = CalcularIdadeMs(servo.UltimoComandoEnviado);
double idadeUltimaRespostaServoMs = CalcularIdadeMs(ultimaRespostaAnguloServo);
bool telemetriaPosComandoServo =
possuiComandoServo &&
@ -820,6 +826,7 @@ namespace AgroBase.Services.Operadores
id,
label = servo.ID,
tipo,
freq_base = DadosSen.FrequenciaBase,
controlar = servo.Controlar,
mandatorio = servo.Mandatorio,
@ -875,15 +882,8 @@ namespace AgroBase.Services.Operadores
bool possuiRespostaRele = ultimaRespostaEstadoRele != DateTime.MinValue;
double idadeUltimoComandoReleMs =
!possuiComandoRele
? 999999
: Math.Max(0, (agora_dt - rele.UltimoComandoEnviado).TotalMilliseconds);
double idadeUltimaRespostaReleMs =
!possuiRespostaRele
? 999999
: Math.Max(0, (agora_dt - ultimaRespostaEstadoRele).TotalMilliseconds);
double idadeUltimoComandoReleMs = CalcularIdadeMs(rele.UltimoComandoEnviado);
double idadeUltimaRespostaReleMs = CalcularIdadeMs(ultimaRespostaEstadoRele);
bool telemetriaPosComandoRele =
possuiComandoRele &&
@ -896,6 +896,7 @@ namespace AgroBase.Services.Operadores
id,
label = rele.ID,
tipo,
freq_base = DadosSen.FrequenciaBase,
controlar = rele.Controlar,
mandatorio = rele.Mandatorio,
dados,
@ -966,15 +967,8 @@ namespace AgroBase.Services.Operadores
? (ultimaRespostaLedId <= ultimaRespostaLedStatus ? ultimaRespostaLedId : ultimaRespostaLedStatus)
: DateTime.MinValue;
double idadeUltimoComandoLedMs =
!possuiComandoLed
? 999999
: Math.Max(0, (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds);
double idadeUltimaRespostaLedMs =
!possuiRespostaLed
? 999999
: Math.Max(0, (agora_dt - ultimaRespostaLed).TotalMilliseconds);
double idadeUltimoComandoLedMs = CalcularIdadeMs(led.UltimoComandoEnviado);
double idadeUltimaRespostaLedMs = CalcularIdadeMs(ultimaRespostaLed);
bool telemetriaPosComandoLed =
possuiComandoLed &&
@ -990,6 +984,7 @@ namespace AgroBase.Services.Operadores
id,
label = led.ID,
tipo,
freq_base = DadosSen.FrequenciaBase,
controlar = led.Controlar,
mandatorio = led.Mandatorio,
@ -1018,27 +1013,22 @@ namespace AgroBase.Services.Operadores
senLedsAtualizados.Add(("sinaleiros.ativos", senLedsAtivos));
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Sen), senLedsAtualizados.ToArray());
#endregion
#region ATU
var DadosAtu = op.DispAtu.Dados;
DateTime ultimaRespostaAtu = DadosAtu.DadosLeitura?.UltimoComandoRespondido ?? DateTime.MinValue;
double idadeUltimaRespostaAtuMs =
ultimaRespostaAtu == DateTime.MinValue
? 999999
: (agora_dt - ultimaRespostaAtu).TotalMilliseconds;
double idadeUltimaRespostaAtuMs = CalcularIdadeMs(ultimaRespostaAtu);
bool pulverizadorAutomatico =
op.Parametros?.Controle?.PulverizadorAutomatico ?? false;
bool pulverizadorAutomatico = op.Parametros?.Controle?.PulverizadorAutomatico ?? false;
double pressaoAlvo =
op.Parametros?.Controle?.AtuPressaoLinha ?? 0;
double pressaoAlvo = op.Parametros?.Controle?.AtuPressaoLinha ?? 0;
double pressaoLinha =
DadosAtu.PressaoLinha;
bool pressaoLinhaRegular =
pressaoAlvo <= 0 || pressaoLinha >= pressaoAlvo - 20;
double pressaoLinha = DadosAtu?.BombaPressurizadora?._PressaoAtual ?? _Sensoriamento?.Atuador?.PressaoLinha ?? 0;
double toleranciaPressao = Math.Max(2.0, pressaoAlvo * 0.15);
bool pressaoLinhaRegular = pressaoAlvo <= 0 || pressaoLinha >= Math.Max(0, pressaoAlvo - toleranciaPressao);
RedisService.AtualizarCampos(
RedisService.ModKey(Enums.T_Code.Atu),
("timestamp", agora),
@ -1091,10 +1081,7 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimaRespostaSensorMs =
sensor.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - sensor.UltimaLeitura).TotalMilliseconds;
double idadeUltimaRespostaSensorMs = CalcularIdadeMs(sensor.UltimaLeitura);
var sensorObj = new
{
@ -1107,6 +1094,7 @@ namespace AgroBase.Services.Operadores
dados,
timestamp = agora,
saude = saude ?? new { },
freq_base = DadosAtu.FrequenciaBase,
ultima_resposta_ms = idadeUltimaRespostaSensorMs,
};
@ -1139,15 +1127,11 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimoComandoBicoMs =
bico.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - bico.UltimoComandoEnviado).TotalMilliseconds;
DateTime ultimoComandoBico = bico.UltimoComandoEnfileirado;
double idadeUltimaRespostaBicoMs =
bico.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - bico.UltimaLeitura).TotalMilliseconds;
double idadeUltimoComandoBicoMs = CalcularIdadeMs(ultimoComandoBico);
double idadeUltimaRespostaBicoMs = CalcularIdadeMs(bico.UltimaLeitura);
var bicoObj = new
{
@ -1160,6 +1144,7 @@ namespace AgroBase.Services.Operadores
dados,
timestamp = agora,
saude = saude ?? new { },
freq_base = DadosAtu.FrequenciaBase,
comando_atuar = bico.ComandoAtuar,
estado_desejado = bico.UltimoStatusDesejado,
@ -1181,15 +1166,15 @@ namespace AgroBase.Services.Operadores
vazao_media_mls = bico.VazaoMediaMLs,
volume_vazado_ml = bico.VolumeVazadoML,
comando_solicitado = bico.ComandoAtuar,
comando_solicitado = bico.Comandar && bico.ComandoAtuar,
status_desejado_fisico = bico.UltimoStatusDesejado,
angulo_desejado_fisico = bico.UltimoAnguloDesejado,
ultimo_comando_ms = idadeUltimoComandoBicoMs,
ultima_resposta_ms = idadeUltimaRespostaBicoMs,
telemetria_pos_comando =
bico.UltimoComandoEnviado != DateTime.MinValue &&
bico.UltimaLeitura != DateTime.MinValue &&
bico.UltimaLeitura >= bico.UltimoComandoEnviado
ultimoComandoBico != DateTime.MinValue &&
bico.UltimaRespostaPosteriorAoComando != DateTime.MinValue &&
bico.UltimaRespostaPosteriorAoComando >= ultimoComandoBico
};
atuBicosAtualizados.Add(($"{basePath}", bicoObj));
@ -1221,15 +1206,11 @@ namespace AgroBase.Services.Operadores
})
.ToList();
double idadeUltimoComandoBombaMs =
bomba.UltimoComandoEnviado == DateTime.MinValue
? 999999
: (agora_dt - bomba.UltimoComandoEnviado).TotalMilliseconds;
DateTime ultimoComandoBomba = bomba.UltimoComandoEnfileirado;
double idadeUltimaRespostaBombaMs =
bomba.UltimaLeitura == DateTime.MinValue
? 999999
: (agora_dt - bomba.UltimaLeitura).TotalMilliseconds;
double idadeUltimoComandoBombaMs = CalcularIdadeMs(ultimoComandoBomba);
double idadeUltimaRespostaBombaMs = CalcularIdadeMs(bomba.UltimaLeitura);
var sensorObj = new
{
@ -1242,6 +1223,7 @@ namespace AgroBase.Services.Operadores
dados,
timestamp = agora,
saude = saude ?? new { },
freq_base = DadosAtu.FrequenciaBase,
comando_atuar = bomba.ComandoAtuar,
estado_desejado = bomba.UltimoStatusDesejado,
@ -1274,12 +1256,12 @@ namespace AgroBase.Services.Operadores
ultimo_comando_ms = idadeUltimoComandoBombaMs,
ultima_resposta_ms = idadeUltimaRespostaBombaMs,
telemetria_pos_comando =
bomba.UltimoComandoEnviado != DateTime.MinValue &&
bomba.UltimaLeitura != DateTime.MinValue &&
bomba.UltimaLeitura >= bomba.UltimoComandoEnviado,
ultimoComandoBomba != DateTime.MinValue &&
bomba.UltimaRespostaPosteriorAoComando != DateTime.MinValue &&
bomba.UltimaRespostaPosteriorAoComando >= ultimoComandoBomba,
desligar_sem_bicos_abertos = bomba.Controle?.DesligarSemBicosAbertos ?? true,
qtd_bicos_ligados = bomba._QtdBicosLigados,
potencia_final = bomba._PotenciaFinal,
potencia_feedforward = bomba._PotenciaFeedForwared,
pressao_sensor = bomba._PressaoSensor,
@ -1300,6 +1282,7 @@ namespace AgroBase.Services.Operadores
atuBombasAtualizados.Add(("bombas.ativos", atuBombasAtivos));
RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Atu), atuBombasAtualizados.ToArray());
#endregion
//var DadosLra = Variaveis.LoraService;

View File

@ -785,7 +785,7 @@ namespace AgroBase.Services
return false;
}
if (!CanManager.CanService.Iniciado)
if (!CanManager.CanService.Iniciado || !CanManager.CanService.IsConnected)
{
bool initialized = CanManager.CanService.Inicializar(250);
@ -1587,8 +1587,7 @@ namespace AgroBase.Services
}
}
public static void AdicionarOuAtualizarDispositivoMapeado(
DispositivoDetalhesModel device)
public static void AdicionarOuAtualizarDispositivoMapeado(DispositivoDetalhesModel device)
{
if (device == null)
return;

View File

@ -56,6 +56,7 @@ class ModuloAtuador(ModuloDiagnosticoBase):
self.padroes_bicos = {}
self.padroes_bombas = {}
self.runtime_bombas = {}
self.runtime_bicos = {}
def atualizar_saude(self):
try:
@ -612,50 +613,149 @@ class ModuloAtuador(ModuloDiagnosticoBase):
dados = item["dados"]
label = dados.get("label")
agora = time.monotonic()
runtime = self._runtime_bico(item)
# ============================================================
# Comando desejado e estado físico
# ============================================================
comando_presente = (
"comando_atuar" in dados or
"estado_desejado" in dados or
"status_desejado_fisico" in dados or
"ultimo_status_desejado" in dados
"comando_atuar" in dados
or "estado_desejado" in dados
or "status_desejado_fisico" in dados
or "ultimo_status_desejado" in dados
)
comando = self._estado_fisico_desejado_ligado(dados)
estado = self._get(dados, "estado_leitura", self._campo_atual(dados, "EstadoLeitura", 0))
estado = self._get(
dados,
"estado_leitura",
self._campo_atual(
dados,
"EstadoLeitura",
0,
),
)
estado_on = self._estado_ligado(estado)
usa_servo = dados.get("com_movimento", False)
# ============================================================
# Controle angular, quando o bico possui movimento
# ============================================================
usa_servo = self._bool(
dados.get("com_movimento", False)
)
angulo = None
angulo_desejado = None
angulo_neutro = None
angulo_controle = None
erro_angulo = 0.0
if usa_servo:
angulo = self._float(
self._get(dados, "angulo_leitura", self._campo_atual(dados, "ServoAnguloLeitura", 0)),
self._get(
dados,
"angulo_leitura",
self._campo_atual(
dados,
"ServoAnguloLeitura",
0,
),
),
0,
)
angulo_neutro = self._float(dados.get("angulo_neutro", 90), 90)
angulo_controle = self._float(dados.get("angulo_controle", dados.get("angulo_sp", 90)), 90)
angulo_neutro = self._float(
dados.get("angulo_neutro", 90),
90,
)
angulo_controle = self._float(
dados.get(
"angulo_controle",
dados.get("angulo_sp", 90),
),
90,
)
angulo_desejado = self._float(
dados.get(
"angulo_desejado_fisico",
dados.get(
"angulo_desejado",
angulo_controle if comando else angulo_neutro
)
(
angulo_controle
if comando
else angulo_neutro
),
),
),
(
angulo_controle
if comando
else angulo_neutro
),
angulo_controle if comando else angulo_neutro,
)
ultimo_comando_ms = self._float(dados.get("ultimo_comando_ms", 999999), 999999)
tem_idade_comando = ultimo_comando_ms < 900000
erro_angulo = abs(
angulo - angulo_desejado
)
score = 100.0
# ============================================================
# Informações temporais
# ============================================================
ultimo_comando_ms = self._float(
dados.get("ultimo_comando_ms", 999999),
999999,
)
ultima_resposta_ms = self._obter_ultima_resposta_ms(
dados
)
tem_comando_fisico = (
self._tem_comando_fisico_registrado(dados)
)
telemetria_fresca = (
ultima_resposta_ms <= self.RESPOSTA_RECENTE_MS
)
telemetria_pos_comando = (
self._telemetria_depois_do_comando(dados)
)
# ============================================================
# Sem informações suficientes sobre o comando
# ============================================================
if not comando_presente:
self._atualizar_persistencia(
runtime,
"comandado_on_leitura_off_desde",
False,
agora,
)
self._atualizar_persistencia(
runtime,
"comandado_off_leitura_on_desde",
False,
agora,
)
self._atualizar_persistencia(
runtime,
"angulo_divergente_desde",
False,
agora,
)
return 92, {
"comando_disponivel": False,
"estado_on": estado_on,
@ -664,104 +764,368 @@ class ModuloAtuador(ModuloDiagnosticoBase):
"angulo_desejado": angulo_desejado,
}
if tem_idade_comando and ultimo_comando_ms < self.COMANDO_GRACE_MS:
return 96, {
"comando_atuar": comando,
"estado_on": estado_on,
"usa_servo": usa_servo,
"angulo": angulo,
"angulo_desejado": angulo_desejado,
"ultimo_comando_ms": ultimo_comando_ms,
}
# ============================================================
# Telemetria velha
#
# A avaliação de resposta do componente já diagnosticará isso.
# Não devemos transformar uma leitura antiga em falha mecânica.
# ============================================================
if self._aguardando_telemetria_pos_comando(dados):
return 94, {
"comando_disponivel": True,
"aguardando_telemetria_pos_comando": True,
"comando_fisico": comando,
"comando_logico": self._comando_logico_ligado(dados),
"estado_on": estado_on,
"usa_servo": usa_servo,
"angulo": angulo,
"angulo_desejado": angulo_desejado,
"ultimo_comando_ms": ultimo_comando_ms,
"ultima_resposta_ms": self._float(dados.get("ultima_resposta_ms", 999999), 999999),
"telemetria_pos_comando": self._telemetria_depois_do_comando(dados),
}
if comando and not estado_on:
severidade = 85 if tem_idade_comando and ultimo_comando_ms >= self.COMANDO_FALHA_MS else 70
motivos.append(f"Bico comandado ligado, mas leitura ainda indica desligado ({label})")
condicoes.append({
"label": "Bico",
"valor": 0,
"severidade": severidade,
"classe": "processo",
"descricao": f"Bico {label} comandado ligado, mas estado lido ainda está desligado",
"acoes": [
"Aguardar próxima atualização CAN.",
"Verificar solenoide do bico se a divergência persistir.",
"Conferir alimentação e retorno do bico.",
],
})
score = min(score, 55 if severidade >= 85 else 75)
if not comando and estado_on:
falha_confirmada = (
tem_idade_comando and
ultimo_comando_ms >= self.COMANDO_FALHA_MS and
self._telemetria_depois_do_comando(dados)
if not telemetria_fresca:
self._atualizar_persistencia(
runtime,
"comandado_on_leitura_off_desde",
False,
agora,
)
severidade = 95 if falha_confirmada else 82
self._atualizar_persistencia(
runtime,
"comandado_off_leitura_on_desde",
False,
agora,
)
motivos.append(f"Bico ligado sem comando ({label})")
condicoes.append({
"label": "Bico",
"valor": 1,
"severidade": severidade,
"classe": "falha_hardware" if falha_confirmada else "processo",
"descricao": f"Bico {label} aparenta estar ligado sem comando",
"acoes": [
"Verificar travamento da solenoide.",
"Verificar se o comando de desligamento chegou ao ATU.",
],
})
self._atualizar_persistencia(
runtime,
"angulo_divergente_desde",
False,
agora,
)
score = min(score, 25 if falha_confirmada else 65)
return 100, {
"comando_disponivel": True,
"avaliacao_suspensa": (
"telemetria do bico desatualizada"
),
"comando_fisico": comando,
"estado_on": estado_on,
"ultima_resposta_ms": ultima_resposta_ms,
}
if usa_servo and angulo is not None and angulo_desejado is not None:
erro_ang = abs(angulo - angulo_desejado)
# ============================================================
# Divergências brutas
# ============================================================
if erro_ang > self.ANGULO_TOL_BICO_GRAUS and (comando or estado_on):
motivos.append(f"Ângulo do bico distante do desejado ({angulo:.1f}°/{angulo_desejado:.1f}°)")
condicoes.append({
"label": "Ângulo bico",
"valor": angulo,
"severidade": min(75, max(30, int(erro_ang * 3))),
"classe": "processo",
"descricao": f"Ângulo do bico {label} distante do desejado",
"acoes": [
"Verificar servo/posição mecânica do bico.",
"Conferir neutro e ângulo de abertura configurados.",
],
})
score = min(score, 78)
divergencia_ligar = (
comando and not estado_on
)
return int(max(0, min(100, round(score)))), {
divergencia_desligar = (
not comando and estado_on
)
divergencia_angulo = (
usa_servo
and angulo is not None
and angulo_desejado is not None
and erro_angulo > self.ANGULO_TOL_BICO_GRAUS
)
# Durante estes contextos, o último comando ON pode estar
# temporariamente antigo em relação ao bloqueio operacional.
ignorar_divergencia_ligar = (
ctx.get("calibrando", False)
or ctx.get("finalizando", False)
or ctx.get("pausa", False)
or ctx.get("emergencia", False)
)
# Durante calibragem, podem existir pulsos físicos controlados
# por uma sequência assíncrona própria.
ignorar_divergencia_desligar = (
ctx.get("calibrando", False)
)
ignorar_divergencia_angulo = (
ctx.get("calibrando", False)
or ctx.get("finalizando", False)
or ctx.get("emergencia", False)
)
# O ângulo só é avaliado quando o estado ON/OFF já está coerente.
avaliar_angulo = (
divergencia_angulo
and not divergencia_ligar
and not divergencia_desligar
and not ignorar_divergencia_angulo
)
# ============================================================
# Persistência das divergências
# ============================================================
tempo_divergencia_ligar = self._atualizar_persistencia(
runtime,
"comandado_on_leitura_off_desde",
(
divergencia_ligar
and not ignorar_divergencia_ligar
),
agora,
)
tempo_divergencia_desligar = self._atualizar_persistencia(
runtime,
"comandado_off_leitura_on_desde",
(
divergencia_desligar
and not ignorar_divergencia_desligar
),
agora,
)
tempo_divergencia_angulo = self._atualizar_persistencia(
runtime,
"angulo_divergente_desde",
avaliar_angulo,
agora,
)
debug = {
"comando_disponivel": True,
"comando_atuar": comando,
"comando_fisico": comando,
"comando_logico": self._comando_logico_ligado(dados),
"estado_on": estado_on,
"usa_servo": usa_servo,
"angulo": angulo,
"angulo_desejado": angulo_desejado,
"angulo_neutro": angulo_neutro,
"angulo_controle": angulo_controle,
"erro_angulo": erro_angulo,
"ultimo_comando_ms": ultimo_comando_ms,
"ultima_resposta_ms": ultima_resposta_ms,
"tem_comando_fisico": tem_comando_fisico,
"telemetria_fresca": telemetria_fresca,
"telemetria_pos_comando": telemetria_pos_comando,
"divergencia_ligar": divergencia_ligar,
"divergencia_desligar": divergencia_desligar,
"divergencia_angulo": divergencia_angulo,
"tempo_divergencia_ligar_s": tempo_divergencia_ligar,
"tempo_divergencia_desligar_s": tempo_divergencia_desligar,
"tempo_divergencia_angulo_s": tempo_divergencia_angulo,
}
# ============================================================
# Contextos nos quais o comando ON não deve ser avaliado
# ============================================================
if divergencia_ligar and ignorar_divergencia_ligar:
debug["avaliacao_suspensa"] = (
"contexto operacional bloqueia comando ON"
)
return 96, debug
if (
divergencia_desligar
and ignorar_divergencia_desligar
):
debug["avaliacao_suspensa"] = (
"bico em calibragem"
)
return 96, debug
# ============================================================
# Aguarda telemetria posterior ao comando
# ============================================================
possui_divergencia = (
divergencia_ligar
or divergencia_desligar
or avaliar_angulo
)
if (
possui_divergencia
and tem_comando_fisico
and not telemetria_pos_comando
):
debug["aguardando_telemetria_pos_comando"] = True
return 94, debug
score = 100.0
alerta_comando_s = (
self.COMANDO_ALERTA_MS / 1000.0
)
falha_comando_s = (
self.COMANDO_FALHA_MS / 1000.0
)
# ============================================================
# Comando ON, mas leitura permanece OFF
# ============================================================
if divergencia_ligar:
if tempo_divergencia_ligar < alerta_comando_s:
debug["divergencia_em_acomodacao"] = True
debug["tipo_divergencia"] = (
"comandado_on_leitura_off"
)
return 96, debug
falha_confirmada = (
tempo_divergencia_ligar >= falha_comando_s
and telemetria_pos_comando
)
severidade = (
92 if falha_confirmada else 70
)
motivos.append(
f"Bico comandado ligado, mas leitura "
f"ainda indica desligado ({label})"
)
condicoes.append({
"label": "Bico",
"valor": 0,
"severidade": severidade,
"classe": (
"falha_hardware"
if falha_confirmada
else "processo"
),
"descricao": (
f"Bico {label} permaneceu desligado "
"apesar do comando físico ON"
),
"acoes": [
"Verificar solenoide do bico.",
"Verificar alimentação e retorno do bico.",
"Conferir se o comando chegou ao ATU.",
],
})
score = min(
score,
45 if falha_confirmada else 75,
)
# ============================================================
# Comando OFF, mas leitura permanece ON
# ============================================================
if divergencia_desligar:
if tempo_divergencia_desligar < 2.5:
debug["divergencia_em_acomodacao"] = True
debug["tipo_divergencia"] = (
"comandado_off_leitura_on"
)
return 96, debug
falha_confirmada = (
tempo_divergencia_desligar >= 5.0
and telemetria_pos_comando
)
severidade = (
95 if falha_confirmada else 82
)
motivos.append(
f"Bico ligado sem comando ({label})"
)
condicoes.append({
"label": "Bico",
"valor": 1,
"severidade": severidade,
"classe": (
"falha_hardware"
if falha_confirmada
else "processo"
),
"descricao": (
f"Bico {label} permaneceu fisicamente ligado "
"apesar do comando OFF"
),
"acoes": [
"Verificar travamento da solenoide.",
"Verificar se o comando de desligamento chegou ao ATU.",
"Conferir a saída física da firmware.",
],
})
score = min(
score,
25 if falha_confirmada else 65,
)
# ============================================================
# Estado correto, mas ângulo permanece divergente
# ============================================================
if avaliar_angulo:
if tempo_divergencia_angulo < alerta_comando_s:
debug["angulo_em_acomodacao"] = True
return 96, debug
falha_confirmada = (
tempo_divergencia_angulo >= falha_comando_s
and telemetria_pos_comando
)
severidade = (
88
if falha_confirmada
else min(
75,
max(
30,
int(erro_angulo * 3),
),
)
)
motivos.append(
f"Ângulo do bico distante do desejado "
f"({angulo:.1f}°/{angulo_desejado:.1f}°)"
)
condicoes.append({
"label": "Ângulo bico",
"valor": angulo,
"severidade": severidade,
"classe": (
"falha_hardware"
if falha_confirmada
else "processo"
),
"descricao": (
f"Ângulo do bico {label} permaneceu "
"distante do desejado"
),
"acoes": [
"Verificar servo e posição mecânica do bico.",
"Conferir neutro e ângulo de abertura.",
"Verificar telemetria angular do bico.",
],
})
score = min(
score,
45 if falha_confirmada else 78,
)
return int(
max(
0,
min(
100,
round(score),
),
)
), debug
# ============================================================
# Bombas
# ============================================================
@ -1369,6 +1733,22 @@ class ModuloAtuador(ModuloDiagnosticoBase):
return self.runtime_bombas[key]
def _runtime_bico(self, item):
key = str(
item.get("key")
or item.get("id")
or item.get("dados", {}).get("label")
or "bico"
)
if key not in self.runtime_bicos:
self.runtime_bicos[key] = {
"comandado_on_leitura_off_desde": None,
"comandado_off_leitura_on_desde": None,
}
return self.runtime_bicos[key]
@staticmethod
def _atualizar_persistencia(runtime, campo, condicao, agora):
"""

View File

@ -1,4 +1,5 @@
import time
import math
from shared.enums import T_Code, StatusModulo
from shared.utils import penalizar_frequencia
from health_worker.modulos.base import ModuloDiagnosticoBase
@ -56,10 +57,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
operacao = ContextoGlobalRedis.get_operacao() or {}
equipamento = ContextoGlobalRedis.get_equipamento() or {}
conectado = self._bool(dados_sen.get("conectado", False))
conectado = self._bool(dados_sen.get("telemetria_recente", dados_sen.get("conectado", False)))
firmware_conectado = self._bool(dados_sen.get("firmware_conectado", conectado))
root_score, root_motivos, root_cond = self._avaliar_root(dados_sen)
sen_disponivel = (conectado and self._ultima_resposta_ms(dados_sen) <= self.RESPOSTA_RECENTE_MS)
sen_disponivel = (conectado and firmware_conectado and self._ultima_resposta_ms(dados_sen) <= self.RESPOSTA_RECENTE_MS)
sensores = self._coletar(dados_sen, "sensores")
servos = self._coletar(dados_sen, "servos")
@ -78,7 +81,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
updates = {}
for item in sensores:
s = self._avaliar_sensor(item, ctx)
s = self._avaliar_sensor(item, ctx, modulo_disponivel=sen_disponivel)
saude_individual.append(s)
updates[f"sensores.{item['key']}.saude"] = s
@ -126,6 +129,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
if not conectado:
saude_final = 0
elif not firmware_conectado:
saude_final = min(saude_final, 75)
if any(self._is_falha_hardware(c) for c in root_cond + processo["condicoes_operacionais"]):
saude_final = min(saude_final, 40)
if processo.get("energia_critica", False):
@ -156,6 +161,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
saude_geral = {
"conectado": conectado,
"telemetria_recente": conectado,
"firmware_conectado": firmware_conectado,
"status": status.value,
"saude": saude_final,
"motivos": self._dedupe(motivos),
@ -196,7 +203,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
# ============================================================
def _avaliar_root(self, dados_sen):
conectado = self._bool(dados_sen.get("conectado", False))
conectado = self._bool(
dados_sen.get(
"telemetria_recente",
dados_sen.get("conectado", False),
)
)
idade_ms = self._ultima_resposta_ms(dados_sen)
motivos = []
condicoes = []
@ -275,22 +287,97 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
# Sensores passivos
# ============================================================
def _avaliar_sensor(self, item, ctx):
def _avaliar_sensor(self, item, ctx, modulo_disponivel=True):
dados = item["dados"]
conectado = self._bool(dados.get("conectado", False))
aferir = self._bool(dados.get("aferir", False))
mandatorio = self._bool(dados.get("mandatorio", False))
delegado_ao_mov = str(dados.get("label", "")).upper().startswith("TMV")
em_uso = aferir and not delegado_ao_mov
agora = time.monotonic()
runtime = self._runtime_saida_binaria(item)
if not modulo_disponivel:
runtime["modulo_disponivel_anterior"] = False
runtime["componente_disponivel_anterior"] = False
return self._item(
item, StatusModulo.DESCONECTADO, 0, [], [], False,
{
"avaliacao_suspensa": True,
"motivo_suspensao": "Módulo SEN indisponível",
},
"sensor",
)
if not runtime["modulo_disponivel_anterior"]:
runtime["modulo_recuperado_em"] = agora
runtime["componente_disponivel_anterior"] = False
runtime["modulo_disponivel_anterior"] = True
tempo_retorno_modulo_ms = (
agora - runtime["modulo_recuperado_em"]
) * 1000.0
if not conectado:
runtime["componente_disponivel_anterior"] = False
if tempo_retorno_modulo_ms < self.MODULO_RECUPERACAO_GRACE_MS:
return self._item(
item, StatusModulo.DESCONECTADO, 0, [], [], False,
{
"avaliacao_suspensa": True,
"motivo_suspensao": "Aguardando sensor após retorno do SEN",
"tempo_desde_retorno_modulo_ms": tempo_retorno_modulo_ms,
},
"sensor",
)
return self._item(
item,
StatusModulo.DESCONECTADO,
0,
["desconectado após período de recuperação"],
[],
em_uso,
{"tempo_desde_retorno_modulo_ms": tempo_retorno_modulo_ms},
"sensor",
)
if not runtime["componente_disponivel_anterior"]:
runtime["componente_recuperado_em"] = agora
runtime["componente_disponivel_anterior"] = True
tempo_retorno_componente_ms = (
agora - runtime["componente_recuperado_em"]
) * 1000.0
if tempo_retorno_componente_ms < self.COMPONENTE_RECUPERACAO_GRACE_MS:
return self._item(
item,
StatusModulo.OPERANTE,
100,
[],
[],
em_uso,
{
"avaliacao_suspensa": True,
"motivo_suspensao": "Estabilização após reconexão do sensor",
"tempo_desde_retorno_componente_ms": tempo_retorno_componente_ms,
"avaliacao_delegada_ao_mov": delegado_ao_mov,
},
"sensor",
)
motivos = []
condicoes = []
if not conectado:
return self._item(item, StatusModulo.DESCONECTADO, 0, ["desconectado"], [], em_uso, {}, "sensor")
resposta_score = self._score_resposta(dados, motivos, condicoes, "Sensor SEN")
valor_score, valor_debug = self._avaliar_valores_sensor(item, ctx, motivos, condicoes)
telemetria = self._avaliar_telemetria(dados, motivos, self._labels_criticos_sensor(dados, ctx, mandatorio))
criticos = self._labels_criticos_sensor(dados, ctx, mandatorio)
valor_score, valor_debug = self._avaliar_valores_sensor(
item, ctx, motivos, condicoes, criticos
)
telemetria = self._avaliar_telemetria(dados, motivos, criticos)
saude = resposta_score * 0.35 + valor_score * 0.45 + telemetria["score"] * 0.20
if any(self._is_falha_hardware(c) for c in condicoes):
@ -298,29 +385,62 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
saude = int(max(0, min(100, round(saude))))
status = self._resolver_status_item(conectado, saude, condicoes)
debug = {
"resposta": resposta_score,
"valores": valor_debug,
"telemetria": telemetria["parametros"],
"avaliacao_delegada_ao_mov": delegado_ao_mov,
}
return self._item(item, status, saude, motivos, condicoes, em_uso, debug, "sensor")
def _avaliar_valores_sensor(self, item, ctx, motivos, condicoes):
return self._item(
item,
status,
saude,
motivos,
condicoes,
em_uso,
{
"resposta": resposta_score,
"valores": valor_debug,
"telemetria": telemetria["parametros"],
"avaliacao_delegada_ao_mov": delegado_ao_mov,
},
"sensor",
)
def _avaliar_valores_sensor(self, item, ctx, motivos, condicoes, criticos):
dados = item["dados"]
sensor_label = str(dados.get("label", ""))
criticos = {self._canon(c) for c in (criticos or []) if c}
score = 100.0
debug = {}
for key, campo in self._campos(dados).items():
label = str(campo.get("label") or key)
valor = self._float(campo.get("atual", 0), 0)
valor = self._float(campo.get("atual"), None)
minimo = campo.get("minimo", None)
maximo = campo.get("maximo", None)
nominal = campo.get("nominal", None)
unidade = campo.get("unidade", campo.get("unidade_medida", ""))
debug[key] = {"label": label, "atual": valor, "minimo": minimo, "maximo": maximo, "nominal": nominal, "unidade": unidade}
if valor is None:
critico = self._canon(label) in criticos
motivos.append(f"{sensor_label}.{label} possui leitura inválida")
condicoes.append({
"label": "Leitura sensor SEN",
"valor": None,
"severidade": 92 if critico else 75,
"classe": "falha_hardware" if critico else "processo",
"descricao": f"{sensor_label}.{label} retornou valor inválido",
"acoes": [
"Verificar sensor, alimentação e cabeamento.",
"Verificar sentinela e conversão da firmware.",
],
})
score = min(score, 0 if critico else 60)
debug[key] = {
"label": label,
"atual": None,
"valido": False,
}
continue
sc, conds, mots = self._avaliar_campo(sensor_label, label, valor, minimo, maximo)
score = min(score, sc)
motivos.extend(mots)
@ -509,6 +629,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
token_desejado = round(angulo_sp, 3)
self._registrar_desejado(runtime, token_desejado)
self._atualizar_evidencia_comando(runtime, evidencia)
idade_desejo_ms = self._idade_desejo_ms(runtime)
debug["idade_desejo_ms"] = idade_desejo_ms
debug["telemetria_confirmada_desejo"] = runtime["telemetria_confirmada_desejo"]
erro = abs(angulo - angulo_sp)
debug["erro"] = erro
@ -532,12 +657,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
debug["avaliacao_suspensa"] = "aguardando primeiro comando"
return 96, debug
if not evidencia["telemetria_pos_comando"]:
if not runtime["telemetria_confirmada_desejo"]:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando"
return 96, debug
if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS:
if idade_desejo_ms < self.COMANDO_GRACE_MS:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "período de resposta ao comando"
return 96, debug
@ -862,6 +987,9 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
"coerencia_desde": None,
"nivel_divergencia": None,
"ultimo_desejado": None,
"desejo_alterado_em": None,
"telemetria_confirmada_desejo": False,
"possui_comando_anterior": False,
})
def _preparar_atuador_auxiliar(
@ -995,10 +1123,18 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
def _registrar_desejado(self, runtime, token_desejado):
if runtime.get("ultimo_desejado") == token_desejado:
return
return False
self._resetar_estado_comando(
runtime,
manter_desejado=False,
)
self._resetar_estado_comando(runtime, manter_desejado=False)
runtime["ultimo_desejado"] = token_desejado
runtime["desejo_alterado_em"] = time.monotonic()
runtime["telemetria_confirmada_desejo"] = False
runtime["possui_comando_anterior"] = False
return True
@staticmethod
def _resetar_estado_comando(runtime, manter_desejado=True):
@ -1007,6 +1143,43 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
runtime["nivel_divergencia"] = None
if not manter_desejado:
runtime["ultimo_desejado"] = None
runtime["desejo_alterado_em"] = None
runtime["telemetria_confirmada_desejo"] = False
runtime["possui_comando_anterior"] = False
@staticmethod
def _atualizar_evidencia_comando(runtime, evidencia):
possui_comando = bool(
evidencia.get("possui_comando", False)
)
if not possui_comando:
runtime["possui_comando_anterior"] = False
runtime["telemetria_confirmada_desejo"] = False
return
if not runtime.get("possui_comando_anterior", False):
runtime["possui_comando_anterior"] = True
runtime["desejo_alterado_em"] = time.monotonic()
runtime["telemetria_confirmada_desejo"] = False
runtime["divergencia_desde"] = None
runtime["coerencia_desde"] = None
runtime["nivel_divergencia"] = None
if evidencia.get("telemetria_pos_comando", False):
runtime["telemetria_confirmada_desejo"] = True
@staticmethod
def _idade_desejo_ms(runtime):
alterado_em = runtime.get("desejo_alterado_em")
if alterado_em is None:
return 999999.0
return max(
0.0,
(time.monotonic() - alterado_em) * 1000.0,
)
def _avaliar_comando_binario(
self,
@ -1019,10 +1192,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
):
dados = item["dados"]
label = dados.get("label")
controlar = self._bool(
dados.get("controlar", False)
)
controlar = self._bool(dados.get("controlar", False))
desejado_presente = any(
chave in dados
@ -1051,89 +1221,95 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
lido = self._estado_ligado(
dados.get(
"estado_leitura",
self._campo_atual(
dados,
"EstadoLeitura",
False,
),
self._campo_atual(dados, "EstadoLeitura", False),
)
)
ultimo_comando_ms = self._float(
dados.get("ultimo_comando_ms", 999999),
999999,
)
ultima_resposta_ms = self._float(
dados.get("ultima_resposta_ms", 999999),
999999,
)
possui_comando = self._bool(
dados.get(
"possui_comando",
ultimo_comando_ms < 900000,
)
)
if "telemetria_pos_comando" in dados:
telemetria_pos_comando = self._bool(
dados.get("telemetria_pos_comando", False)
)
else:
# Compatibilidade com publicação antiga.
telemetria_pos_comando = (
possui_comando and
ultima_resposta_ms <= ultimo_comando_ms + 100.0
)
evidencia = self._evidencia_comando(dados)
debug = {
"controlar": controlar,
"estado_lido": lido,
"estado_desejado": desejado,
"possui_comando": possui_comando,
"telemetria_pos_comando": telemetria_pos_comando,
"ultimo_comando_ms": ultimo_comando_ms,
"ultima_resposta_ms": ultima_resposta_ms,
**evidencia,
}
if not controlar:
runtime["divergencia_desde"] = None
self._resetar_estado_comando(runtime, manter_desejado=False)
return 100, debug
if not desejado_presente or not leitura_presente:
runtime["divergencia_desde"] = None
self._resetar_estado_comando(runtime, manter_desejado=False)
debug["contrato_comando_completo"] = False
return 92, debug
# Estado coerente: limpa qualquer divergência anterior.
token_desejado = bool(desejado)
self._registrar_desejado(runtime, token_desejado)
self._atualizar_evidencia_comando(runtime, evidencia)
idade_desejo_ms = self._idade_desejo_ms(runtime)
debug["idade_desejo_ms"] = idade_desejo_ms
debug["telemetria_confirmada_desejo"] = runtime["telemetria_confirmada_desejo"]
if lido == desejado:
runtime["divergencia_desde"] = None
debug["divergente"] = False
return 100, debug
if runtime["nivel_divergencia"] is None:
runtime["coerencia_desde"] = None
return 100, debug
agora = time.monotonic()
if runtime["coerencia_desde"] is None:
runtime["coerencia_desde"] = agora
tempo_coerente_ms = (
agora - runtime["coerencia_desde"]
) * 1000.0
debug["recuperacao_em_confirmacao"] = True
debug["tempo_coerente_ms"] = tempo_coerente_ms
if tempo_coerente_ms >= self.COMANDO_RECUPERACAO_MS:
runtime["nivel_divergencia"] = None
runtime["coerencia_desde"] = None
return 100, debug
motivos.append(
f"{label} recuperou o estado desejado; confirmando estabilidade"
)
condicoes.append({
"label": nome,
"valor": int(lido),
"severidade": 55,
"classe": "processo",
"descricao": (
f"{label} voltou ao estado desejado e está em confirmação"
),
"acoes": [
"Aguardar confirmação da estabilidade do retorno.",
],
})
return 78, debug
debug["divergente"] = True
runtime["coerencia_desde"] = None
# Não existe evidência de que o estado desejado foi enviado.
if not possui_comando:
if not evidencia["possui_comando"]:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando primeiro comando"
return 96, debug
# A resposta ainda é antiga em relação ao comando.
if not telemetria_pos_comando:
if not runtime["telemetria_confirmada_desejo"]:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = (
"aguardando telemetria posterior ao comando"
)
return 96, debug
# Comando acabou de ser enviado.
if ultimo_comando_ms < self.COMANDO_GRACE_MS:
if idade_desejo_ms < self.COMANDO_GRACE_MS:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = (
"período de resposta ao comando"
)
debug["avaliacao_suspensa"] = "período de resposta ao comando"
return 96, debug
agora = time.monotonic()
@ -1147,16 +1323,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
< self.COMPONENTE_RECUPERACAO_GRACE_MS
):
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = (
"estabilização após reconexão"
)
debug["avaliacao_suspensa"] = "estabilização após reconexão"
debug["tempo_desde_retorno_componente_ms"] = (
tempo_desde_retorno_componente_ms
)
return 96, debug
# A persistência começa quando a divergência é efetivamente
# observada em condições válidas.
if runtime["divergencia_desde"] is None:
runtime["divergencia_desde"] = agora
@ -1166,39 +1338,54 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
debug["tempo_divergente_ms"] = tempo_divergente_ms
if runtime["nivel_divergencia"] == "falha":
motivos.append(f"{label} não acompanhou o estado desejado")
condicoes.append({
"label": nome,
"valor": int(lido),
"severidade": severidade_falha,
"classe": "falha_hardware",
"descricao": f"{label} permaneceu diferente do estado desejado",
"acoes": [
"Verificar saída física do relé.",
"Verificar carga conectada.",
"Verificar comando e retorno do SEN.",
],
})
return 30, debug
if tempo_divergente_ms < self.COMANDO_ALERTA_MS:
return 96, debug
if tempo_divergente_ms < self.COMANDO_FALHA_MS:
runtime["nivel_divergencia"] = "alerta"
motivos.append(
f"{label} ainda não acompanhou o estado desejado"
)
condicoes.append({
"label": nome,
"valor": int(lido),
"severidade": 78,
"classe": "processo",
"descricao":
f"{label} apresenta divergência persistente de comando",
"descricao": (
f"{label} apresenta divergência persistente de comando"
),
"acoes": [
"Aguardar confirmação ou verificar saída física.",
],
})
return 68, debug
motivos.append(
f"{label} não acompanhou o estado desejado"
)
runtime["nivel_divergencia"] = "falha"
motivos.append(f"{label} não acompanhou o estado desejado")
condicoes.append({
"label": nome,
"valor": int(lido),
"severidade": severidade_falha,
"classe": "falha_hardware",
"descricao":
f"{label} permaneceu diferente do estado desejado",
"descricao": f"{label} permaneceu diferente do estado desejado",
"acoes": [
"Verificar saída física do relé.",
"Verificar carga conectada.",
@ -1319,6 +1506,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
st_sp if status_sp_presente else None,
)
self._registrar_desejado(runtime, token_desejado)
self._atualizar_evidencia_comando(runtime, evidencia)
idade_desejo_ms = self._idade_desejo_ms(runtime)
debug["idade_desejo_ms"] = idade_desejo_ms
debug["telemetria_confirmada_desejo"] = runtime["telemetria_confirmada_desejo"]
# Se o SP existe, uma leitura ausente (-1) também é divergência.
coerente_id = (
@ -1366,12 +1558,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
debug["avaliacao_suspensa"] = "aguardando primeiro comando"
return 96, debug
if not evidencia["telemetria_pos_comando"]:
if not runtime["telemetria_confirmada_desejo"]:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando"
return 96, debug
if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS:
if idade_desejo_ms < self.COMANDO_GRACE_MS:
runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "período de resposta ao comando"
return 96, debug
@ -1653,6 +1845,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
if not conectado:
return StatusModulo.DESCONECTADO
em_uso = [x for x in saude_individual if self._bool(x.get("em_uso", False))]
if any(x.get("status") == StatusModulo.DESCONECTADO.value and self._bool(x.get("mandatorio", False)) for x in em_uso):
return StatusModulo.FALHA
if any(self._is_falha_hardware(c) for c in condicoes):
return StatusModulo.FALHA
if any(x.get("status") == StatusModulo.FALHA.value for x in em_uso):
@ -1665,6 +1859,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
return StatusModulo.ALERTA
if any(x.get("status") == StatusModulo.ALERTA.value for x in em_uso):
return StatusModulo.ALERTA
if any(x.get("status") == StatusModulo.DESCONECTADO.value for x in em_uso):
return StatusModulo.ALERTA
return StatusModulo.OPERANTE
@staticmethod
@ -1795,14 +1991,24 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
@staticmethod
def _float(valor, padrao=0.0):
fallback = None if padrao is None else float(padrao)
try:
if valor is None:
return float(padrao)
return fallback
if hasattr(valor, "value"):
valor = valor.value
return float(valor)
numero = float(valor)
if not math.isfinite(numero):
return fallback
return numero
except Exception:
return float(padrao)
return fallback
@staticmethod
def _safe_int(valor, padrao=0):

View File

@ -78,6 +78,31 @@ public:
// Mantem o watchdog de tasks como ultimo cinto de seguranca contra firmware
// realmente travado. Ele nao e usado para decidir se o barramento esta vivo.
esp_task_wdt_init(TASK_WDT_SECONDS, true);
// PARA VERSAO 3.3.11 do CORE
/*esp_task_wdt_config_t wdtConfig = {};
wdtConfig.timeout_ms = static_cast<uint32_t>(TASK_WDT_SECONDS) * 1000UL;
// Nao inscrevemos automaticamente as idle tasks.
// Nosso CanService registra explicitamente apenas as tasks
// que deseja supervisionar.
wdtConfig.idle_core_mask = 0;
wdtConfig.trigger_panic = true;
esp_err_t wdtErr = esp_task_wdt_reconfigure(&wdtConfig);
// Se ainda nao tiver sido inicializado pelo core,
// inicializa agora.
if (wdtErr == ESP_ERR_INVALID_STATE) {
wdtErr = esp_task_wdt_init(&wdtConfig);
}
if (wdtErr != ESP_OK) {
PrintTela("[CAN] Falha ao configurar Task WDT: " + String((int)wdtErr));
}*/
esp_task_wdt_add(CanTaskRxHandle);
esp_task_wdt_add(CanTaskTxHandle);
esp_task_wdt_add(CallbackWorkerHandle);

View File

@ -76,6 +76,11 @@ class I2CService {
uint32_t readyDropsAds = 0;
uint32_t falhasIoAds = 0;
uint32_t wireTimeoutMs = 0;
// Recovery local: nao altera a geracao do Wire.
uint32_t recoveriesLocaisMux = 0;
uint32_t recoveriesLocaisAds = 0;
uint32_t falhasLocaisDispositivos = 0;
};
static DiagnosticoCAN ObterDiagnosticoCAN() {
@ -121,6 +126,10 @@ class I2CService {
d.readyDropsAds = _readyDropsAds;
d.falhasIoAds = _falhasIoAds;
d.recoveriesLocaisMux = _recoveriesLocaisMux;
d.recoveriesLocaisAds = _recoveriesLocaisAds;
d.falhasLocaisDispositivos = _falhasLocaisDispositivos;
portEXIT_CRITICAL(&_estadoLock);
d.infraestruturaMuxOk = (d.muxEsperadoMask & d.muxValidadoMask) == d.muxEsperadoMask;
@ -390,8 +399,58 @@ class I2CService {
return RecuperarBarramento(motivo, ignorarCooldown);
}
/*
* Falha de um dispositivo individual NAO e falha global do barramento.
*
* O proprio model do sensor continua responsavel por:
* - contar falhas consecutivas;
* - marcar Iniciado=false;
* - tentar Inicializar() novamente.
*
* O I2CService apenas registra a degradacao local. Isso evita que um
* unico sensor derrube a geracao de todos os outros.
*/
static void NotificarFalhaDispositivo(int idSensor, const String& motivo) {
RegistrarFalhaTransacao("ID " + String(idSensor) + " | " + motivo);
RegistrarFalhaLocalDispositivo(idSensor, motivo);
}
static void NotificarSucessoDispositivo(int idSensor) {
RegistrarSucessoLocalDispositivo(idSensor);
}
/*
* Use SOMENTE depois que o model do sensor esgotar suas tentativas locais
* de reinicializacao. Esta e a ponte explicita entre recovery local e uma
* nova geracao global.
*
* Nao chamar por amostra invalida isolada.
*/
static void NotificarFalhaPersistenteDispositivo(
int idSensor,
const String& motivo
) {
RegistrarFalhaLocalDispositivo(
idSensor,
"persistente | " + motivo
);
AgendarRecovery(
"Dispositivo nao reviveu apos retries locais | ID " +
String(idSensor) + " | " + motivo
);
}
/*
* Por padrao mantemos o ultimo canal do MUX selecionado entre acessos.
* No SEN isso e seguro porque os INAs downstream usam 0x40 e os
* dispositivos do barramento principal usam outros enderecos.
*
* Isso reduz praticamente pela metade as escritas no TCA9548A.
* Pode ser reativado para outras topologias que precisem isolar o ramal
* ao final de cada acesso.
*/
static void DefinirDesabilitarMuxAoLiberar(bool ativo) {
_desabilitarMuxAoLiberar = ativo;
}
static bool CanalMuxEmQuarentena(uint8_t canalGlobal) {
@ -578,6 +637,9 @@ class I2CService {
s += " SCL=" + String((_pinoSCL >= 0) ? digitalRead(_pinoSCL) : -1);
s += " q_mux=" + String(QuantidadeCanaisMuxEmQuarentena());
s += " q_ads=" + String(QuantidadeADSEmQuarentena());
s += " rec_local_mux=" + String(_recoveriesLocaisMux);
s += " rec_local_ads=" + String(_recoveriesLocaisAds);
s += " falhas_locais=" + String(_falhasLocaisDispositivos);
if (_pinoTcaRdy >= 0) {
s += " tca_rdy=" + String(digitalRead(_pinoTcaRdy) == HIGH ? 1 : 0);
@ -761,6 +823,7 @@ class I2CService {
ContextoBarramento contexto = _contextoAtual;
if (
_desabilitarMuxAoLiberar &&
contexto.valido &&
contexto.canalMuxSolicitado >= 0 &&
!RecoveryPendente()
@ -944,65 +1007,240 @@ class I2CService {
return TrocarCanalMux(enderecoMux + indiceMux, canal);
}
static bool TrocarCanalMux(uint8_t endereco, uint8_t canalMux) {
int indiceMux = IndiceMux(endereco);
static bool EscreverMascaraMuxSemRegistrar(
uint8_t endereco,
uint8_t mascara,
uint8_t& erro,
uint32_t& duracaoMs
) {
const uint32_t inicio = millis();
if (indiceMux < 0 || canalMux >= 8) {
MostrarLog(
"MUX",
"Endereco ou canal invalido | endereco=0x" +
String(endereco, HEX) + " canal=" + String(canalMux)
);
Wire.beginTransmission(endereco);
Wire.write(mascara);
erro = Wire.endTransmission();
duracaoMs =
(uint32_t)(millis() - inicio);
return erro == 0;
}
/*
* Recovery local do TCA9548A.
*
* Reseta SOMENTE o MUX, revalida o endereco e volta a selecionar o
* canal desejado. Nao desmonta Wire e nao altera a geracao.
*/
static bool RecuperarMuxLocalSemGeracao(
uint8_t endereco,
uint8_t canalMux
) {
const int indiceMux =
IndiceMux(endereco);
if (
indiceMux < 0 ||
canalMux >= 8
) {
return false;
}
portENTER_CRITICAL(&_estadoLock);
_muxEsperadoMask |= (uint8_t)(1U << indiceMux);
if (_recoveriesLocaisMux < UINT32_MAX) {
_recoveriesLocaisMux++;
}
portEXIT_CRITICAL(&_estadoLock);
uint8_t canalGlobal = (indiceMux * 10) + canalMux;
MostrarLog(
"MUX",
"Recovery LOCAL | endereco=0x" +
String(endereco, HEX) +
" canal=" + String(canalMux) +
" | geracao=" +
String(GeracaoBarramento())
);
int canalAnterior = CanalGlobalAtivoAtual();
int sdaAntes = digitalRead(_pinoSDA);
int sclAntes = digitalRead(_pinoSCL);
int rdyAntes = TCAReadyOk() ? 1 : 0;
uint32_t inicio = millis();
if (CanalMuxEmQuarentena(canalGlobal)) {
MostrarLog("MUX", "Canal bloqueado por quarentena | canalGlobal=" + String(canalGlobal));
if (!ResetarMuxSePossivel()) {
return false;
}
if (!_muxValidado[indiceMux]) {
MostrarLog("MUX", "Validando MUX em 0x" + String(endereco, HEX));
if (!TCAReadyOk()) {
return false;
}
if (!VerificaEnderecoBarramento(endereco)) {
MostrarLog("MUX", "MUX nao encontrado em 0x" + String(endereco, HEX));
return false;
if (
!PingEnderecoSemRegistrar(
endereco,
80
)
) {
return false;
}
_muxValidado[indiceMux] = true;
MuxIniciado = true;
uint8_t erro = 0xFF;
uint32_t duracao = 0;
const bool ok =
EscreverMascaraMuxSemRegistrar(
endereco,
(uint8_t)(1U << canalMux),
erro,
duracao
);
if (!ok) {
MostrarLog(
"MUX",
"Recovery LOCAL nao conseguiu selecionar canal"
" | erro=" + String(erro) +
" duracao=" + String(duracao) + "ms"
);
return false;
}
enderecoMuxAtivo = endereco;
canalAtivo = canalMux;
MostrarLog(
"MUX",
"Recovery LOCAL concluido"
" | canal=" + String(canalMux) +
" | geracao=" +
String(GeracaoBarramento())
);
return true;
}
static bool TrocarCanalMux(
uint8_t endereco,
uint8_t canalMux
) {
const int indiceMux =
IndiceMux(endereco);
if (
indiceMux < 0 ||
canalMux >= 8
) {
MostrarLog(
"MUX",
"Endereco ou canal invalido | endereco=0x" +
String(endereco, HEX) +
" canal=" +
String(canalMux)
);
return false;
}
portENTER_CRITICAL(&_estadoLock);
_muxEsperadoMask |=
(uint8_t)(1U << indiceMux);
portEXIT_CRITICAL(&_estadoLock);
const uint8_t canalGlobal =
(uint8_t)(
(indiceMux * 10) +
canalMux
);
if (
CanalMuxEmQuarentena(
canalGlobal
)
) {
MostrarLog(
"MUX",
"Canal bloqueado por quarentena"
" | canalGlobal=" +
String(canalGlobal)
);
return false;
}
/*
* Se o MUX ainda nao foi validado, primeiro tentamos uma descoberta
* passiva. Ausencia momentanea NAO soma falha global.
*/
if (!_muxValidado[indiceMux]) {
const bool encontrado =
PingEnderecoSemRegistrar(
endereco,
80
);
if (!encontrado) {
if (
!RecuperarMuxLocalSemGeracao(
endereco,
canalMux
)
) {
RegistrarFalhaTransacao(
"MUX nao respondeu apos recovery local"
);
return false;
}
RegistrarSucessoTransacao();
return true;
}
_muxValidado[indiceMux] = true;
MuxIniciado = true;
}
if (enderecoMuxAtivo == endereco && canalAtivo == canalMux) {
if (
enderecoMuxAtivo == endereco &&
canalAtivo == canalMux
) {
return true;
}
// Evita deixar dois ramais conectados simultaneamente.
if (enderecoMuxAtivo != 0xFF && enderecoMuxAtivo != endereco) {
Wire.beginTransmission(enderecoMuxAtivo);
Wire.write(0x00);
int erroDesabilitar = Wire.endTransmission();
/*
* SEN atual possui um unico TCA9548A. Este bloco preserva suporte a
* multiplos MUXes: se um endereco diferente ficou selecionado,
* desabilita-o antes de ativar o proximo.
*/
if (
enderecoMuxAtivo != 0xFF &&
enderecoMuxAtivo != endereco
) {
uint8_t erroDesabilitar = 0xFF;
uint32_t duracaoDesabilitar = 0;
if (erroDesabilitar != 0) {
bool desabilitou =
EscreverMascaraMuxSemRegistrar(
enderecoMuxAtivo,
0x00,
erroDesabilitar,
duracaoDesabilitar
);
if (!desabilitou) {
MostrarLog(
"MUX",
"Falha ao desabilitar MUX anterior 0x" +
"Falha ao desabilitar MUX anterior"
" | endereco=0x" +
String(enderecoMuxAtivo, HEX) +
" | erro=" + String(erroDesabilitar)
" erro=" +
String(erroDesabilitar) +
" duracao=" +
String(duracaoDesabilitar) +
"ms"
);
RegistrarFalhaTransacao("Falha ao desabilitar MUX anterior");
RegistrarFalhaTransacao(
"Falha ao desabilitar MUX anterior"
);
return false;
}
@ -1010,51 +1248,84 @@ class I2CService {
canalAtivo = -1;
}
Wire.beginTransmission(endereco);
Wire.write((uint8_t)(1U << canalMux));
int erro = Wire.endTransmission();
const int canalAnterior =
CanalGlobalAtivoAtual();
uint32_t duracao = (uint32_t)(millis() - inicio);
int sdaDepois = digitalRead(_pinoSDA);
int sclDepois = digitalRead(_pinoSCL);
int rdyDepois = TCAReadyOk() ? 1 : 0;
uint8_t ultimoErro = 0xFF;
uint32_t ultimaDuracao = 0;
MostrarLog(
"MUX",
"Troca"
" | anterior=" + String(canalAnterior) +
" solicitado=" + String(canalGlobal) +
" erro=" + String(erro) +
" duracao=" + String(duracao) + "ms" +
" antes[SDA=" + String(sdaAntes) +
" SCL=" + String(sclAntes) +
" RDY=" + String(rdyAntes) + "]" +
" depois[SDA=" + String(sdaDepois) +
" SCL=" + String(sclDepois) +
" RDY=" + String(rdyDepois) + "]"
);
for (
uint8_t tentativa = 0;
tentativa <
TentativasTrocaMuxAntesResetLocal;
tentativa++
) {
const bool ok =
EscreverMascaraMuxSemRegistrar(
endereco,
(uint8_t)(1U << canalMux),
ultimoErro,
ultimaDuracao
);
if (ok) {
enderecoMuxAtivo = endereco;
canalAtivo = canalMux;
RegistrarSucessoTransacao();
return true;
}
if (erro != 0) {
MostrarLog(
"MUX",
"Falha ao trocar canal | endereco=0x" + String(endereco, HEX) +
" canal=" + String(canalMux) + " erro=" + String(erro)
"Falha local ao selecionar canal"
" | anterior=" +
String(canalAnterior) +
" solicitado=" +
String(canalGlobal) +
" tentativa=" +
String(tentativa + 1) +
" erro=" +
String(ultimoErro) +
" duracao=" +
String(ultimaDuracao) +
"ms"
" SDA=" +
String(digitalRead(_pinoSDA)) +
" SCL=" +
String(digitalRead(_pinoSCL)) +
" RDY=" +
String(TCAReadyOk() ? 1 : 0)
);
RegistrarFalhaTransacao("Falha ao trocar canal MUX");
return false;
DelayMs(2);
}
enderecoMuxAtivo = endereco;
canalAtivo = canalMux;
RegistrarSucessoTransacao();
/*
* As tentativas simples falharam. Antes de tocar no Wire, recupera
* SOMENTE o MUX e tenta novamente.
*/
if (
RecuperarMuxLocalSemGeracao(
endereco,
canalMux
)
) {
RegistrarSucessoTransacao();
return true;
}
MostrarLog(
"MUX",
"Canal ativo | endereco=0x" + String(endereco, HEX) +
" canal=" + String(canalMux)
/*
* Somente agora uma falha do MUX alimenta o contador GLOBAL.
* Sao necessarias LimiteFalhasTransacaoRecovery ocorrencias
* consecutivas deste nivel para gerar uma nova geracao.
*/
RegistrarFalhaTransacao(
"MUX nao reviveu apos retries e reset local"
);
return true;
return false;
}
static int SelecionarCanalADS(uint8_t canalGlobal) {
@ -1106,6 +1377,11 @@ class I2CService {
const bool leituraOk = LerADSSingleShotComTimeout(canal, leituraADC);
if (!leituraOk) {
TratarFalhaADSLocal(
idSensor,
"falha de transporte/conversao"
);
leituraFalhou = true;
break;
}
@ -1127,6 +1403,14 @@ class I2CService {
leiturasConcluidas++;
}
if (
!leituraFalhou &&
leiturasConcluidas == leituras
) {
ResetarFalhasAdsLocais();
RegistrarSucessoLocalDispositivo(idSensor);
}
if (RecoveryPendente()) {
leituraFalhou = true;
}
@ -1323,9 +1607,10 @@ class I2CService {
static ContextoBarramento _contextoFalha;
static portMUX_TYPE _estadoLock;
// Transacoes deste SEN sao de poucos bytes a 100 kHz.
// 25 ms ainda e uma margem muito grande, mas limita melhor uma
// transferencia realmente emperrada.
// No core 2.0.11 existem caminhos do Wire que podem ignorar este
// limite e retornar apenas ~1 s depois. Mantemos 25 ms como intencao do
// driver, mas o recovery local evita transformar esse stall em reboot
// global imediato.
static constexpr uint32_t TimeoutWireMs = 25;
static constexpr uint32_t LimiteReadyLowMs = 200;
static constexpr uint32_t CooldownRecoveryMs = 1000;
@ -1350,6 +1635,24 @@ class I2CService {
static uint32_t _readyDropsAds;
static uint32_t _falhasIoAds;
// Saude/recovery local. Nenhum destes contadores muda a geracao.
static uint32_t _recoveriesLocaisMux;
static uint32_t _recoveriesLocaisAds;
static uint32_t _falhasLocaisDispositivos;
static uint8_t _falhasLocaisPorId[256];
static uint8_t _falhasAdsConsecutivas;
static uint8_t _falhasRecoveryLocalAdsConsecutivas;
static uint32_t _ultimoRecoveryLocalAdsMs;
static bool _desabilitarMuxAoLiberar;
static constexpr uint8_t LimiteFalhasAdsAntesRecoveryLocal = 3;
static constexpr uint8_t LimiteFalhasRecoveryLocalAdsAntesGlobal = 2;
static constexpr uint32_t CooldownRecoveryLocalAdsMs = 500;
static constexpr uint8_t TentativasTrocaMuxAntesResetLocal = 2;
static void MostrarLog(const String& componente, const String& mensagem) {
if (DebugMode) {
PrintTela("[" + componente + "] " + mensagem);
@ -1975,6 +2278,195 @@ class I2CService {
return total;
}
static void RegistrarFalhaLocalDispositivo(
int idSensor,
const String& motivo
) {
uint8_t consecutivas = 0;
portENTER_CRITICAL(&_estadoLock);
if (_falhasLocaisDispositivos < UINT32_MAX) {
_falhasLocaisDispositivos++;
}
if (idSensor >= 0 && idSensor <= 255) {
uint8_t& valor =
_falhasLocaisPorId[(uint8_t)idSensor];
if (valor < 255) {
valor++;
}
consecutivas = valor;
}
portEXIT_CRITICAL(&_estadoLock);
MostrarLog(
"I2C/LOCAL",
"ID " + String(idSensor) +
" | falha local #" + String(consecutivas) +
" | " + motivo
);
}
static void RegistrarSucessoLocalDispositivo(int idSensor) {
if (idSensor < 0 || idSensor > 255) {
return;
}
portENTER_CRITICAL(&_estadoLock);
_falhasLocaisPorId[(uint8_t)idSensor] = 0;
portEXIT_CRITICAL(&_estadoLock);
}
static void ResetarFalhasAdsLocais() {
portENTER_CRITICAL(&_estadoLock);
_falhasAdsConsecutivas = 0;
_falhasRecoveryLocalAdsConsecutivas = 0;
portEXIT_CRITICAL(&_estadoLock);
}
static void RegistrarFalhaAdsConsecutiva() {
portENTER_CRITICAL(&_estadoLock);
if (_falhasAdsConsecutivas < 255) {
_falhasAdsConsecutivas++;
}
portEXIT_CRITICAL(&_estadoLock);
}
static uint8_t FalhasAdsConsecutivas() {
uint8_t valor;
portENTER_CRITICAL(&_estadoLock);
valor = _falhasAdsConsecutivas;
portEXIT_CRITICAL(&_estadoLock);
return valor;
}
/*
* Recovery local do ADS:
* ADS power-cycle -> ping -> begin -> gain/rate
*
* Nao toca em Wire.end()/Wire.begin(), logo NAO altera a geracao e
* nao invalida INAs/MUX/outros sensores.
*
* Esta funcao e chamada com ownership do i2cMutex.
*/
static bool RecuperarADSLocalSemGeracao() {
const uint32_t agora = millis();
if (
_ultimoRecoveryLocalAdsMs > 0 &&
!TempoPassou(
_ultimoRecoveryLocalAdsMs,
CooldownRecoveryLocalAdsMs
)
) {
return false;
}
_ultimoRecoveryLocalAdsMs = agora;
portENTER_CRITICAL(&_estadoLock);
if (_recoveriesLocaisAds < UINT32_MAX) {
_recoveriesLocaisAds++;
}
portEXIT_CRITICAL(&_estadoLock);
MostrarLog(
"ADS",
"Recovery LOCAL iniciado | sem alterar geracao"
);
AdsIniciado = false;
ultimoEnderecoADS = 0xFF;
if (_pinoAdsPwrEn >= 0) {
ResetarADSSePossivel();
}
else {
DelayMs(2);
}
const bool pingOk =
PingEnderecoSemRegistrar(
enderecoAds,
80
);
const bool iniciou =
pingOk &&
IniciarADSSemMutex(
enderecoAds
);
if (iniciou) {
ResetarFalhasAdsLocais();
MostrarLog(
"ADS",
"Recovery LOCAL concluido | geracao=" +
String(GeracaoBarramento())
);
return true;
}
uint8_t falhasRecovery;
portENTER_CRITICAL(&_estadoLock);
if (
_falhasRecoveryLocalAdsConsecutivas <
255
) {
_falhasRecoveryLocalAdsConsecutivas++;
}
falhasRecovery =
_falhasRecoveryLocalAdsConsecutivas;
portEXIT_CRITICAL(&_estadoLock);
MostrarLog(
"ADS",
"Recovery LOCAL falhou | tentativas=" +
String(falhasRecovery)
);
if (
falhasRecovery >=
LimiteFalhasRecoveryLocalAdsAntesGlobal
) {
AgendarRecovery(
"ADS nao reviveu apos recoveries locais"
);
}
return false;
}
static void TratarFalhaADSLocal(int idSensor, const String& motivo) {
RegistrarFalhaLocalDispositivo(
idSensor,
"ADS | " + motivo
);
RegistrarFalhaAdsConsecutiva();
if (
FalhasAdsConsecutivas() >=
LimiteFalhasAdsAntesRecoveryLocal
) {
(void)RecuperarADSLocalSemGeracao();
}
}
static void RegistrarFalhaTransacao(const String& motivo) {
uint32_t falhas;
@ -2455,7 +2947,13 @@ class I2CService {
static void RegistrarTimeoutADS(uint32_t inicioMs, const char* motivo) {
AtualizarMaxDuracaoADS(inicioMs);
IncrementarTimeoutADS();
RegistrarFalhaTransacao(motivo);
// Timeout isolado do ADS e degradacao LOCAL.
MostrarLog(
"ADS",
String(motivo) +
" | tratado como falha local"
);
}
static void RegistrarReadyDropADS(uint32_t inicioMs) {
@ -2469,6 +2967,7 @@ class I2CService {
portEXIT_CRITICAL(&_estadoLock);
// READY pertence a infraestrutura global do barramento.
RegistrarFalhaTransacao("READY caiu durante leitura ADS");
}
@ -2481,10 +2980,14 @@ class I2CService {
AtualizarMaxDuracaoADS(inicioMs);
IncrementarFalhaIoADS();
RegistrarFalhaTransacao(
String("ADS ") + etapa +
// Erro de I/O do ADS continua local. O chamador decide quando
// tentar power-cycle do ADS, sem alterar a geracao.
MostrarLog(
"ADS",
String(etapa) +
" | erro=" + String(erro) +
" | duracao=" + String(duracaoMs) + "ms"
" | duracao=" +
String(duracaoMs) + "ms"
);
}
@ -2518,7 +3021,10 @@ class I2CService {
endereco == 0xFF ||
IndiceADS(endereco) < 0
) {
RegistrarFalhaTransacao("ADS sem endereco ativo");
MostrarLog(
"ADS",
"Sem endereco ativo para leitura"
);
return false;
}
@ -2769,4 +3275,16 @@ uint32_t I2CService::_timeoutsAds = 0;
uint32_t I2CService::_readyDropsAds = 0;
uint32_t I2CService::_falhasIoAds = 0;
uint32_t I2CService::_recoveriesLocaisMux = 0;
uint32_t I2CService::_recoveriesLocaisAds = 0;
uint32_t I2CService::_falhasLocaisDispositivos = 0;
uint8_t I2CService::_falhasLocaisPorId[256] = {0};
uint8_t I2CService::_falhasAdsConsecutivas = 0;
uint8_t I2CService::_falhasRecoveryLocalAdsConsecutivas = 0;
uint32_t I2CService::_ultimoRecoveryLocalAdsMs = 0;
bool I2CService::_desabilitarMuxAoLiberar = false;
#endif

View File

@ -108,6 +108,9 @@ class PinoModel {
if (barramento == CONTROLADOR) {
if (tipo == _PWM) {
ledcDetachPin(num);
// PARA VERSAO 3.3.11 do core
//ledcDetach(static_cast<uint8_t>(num));
}
pinMode(num, INPUT);
}

View File

@ -48,11 +48,19 @@ class SensorCorrente_v2 : public ComponenteCAN {
}
bool EstaIniciado() {
const uint32_t geracaoAtual = I2CService::GeracaoBarramento();
bool iniciado;
uint32_t geracaoInicializacao;
portENTER_CRITICAL(&_snapshotLock);
iniciado = Iniciado;
geracaoInicializacao = _geracaoI2CInicializacao;
portEXIT_CRITICAL(&_snapshotLock);
return iniciado;
return iniciado &&
geracaoAtual != 0 &&
geracaoInicializacao == geracaoAtual;
}
uint8_t FalhasConsecutivas() {
@ -84,7 +92,7 @@ class SensorCorrente_v2 : public ComponenteCAN {
isINA219 = (_A_Shunt == TiposShuntCorrente::INA219_3A2);
bool encontrado = I2CService::VerificaEnderecoBarramento(_Endereco);
bool encontrado = I2CService::VerificaEnderecoBarramentoPassivo(_Endereco);
if (!encontrado) {
AtualizarEstadoIniciado(false);
MostrarLog("Endereco I2C nao encontrado: " + String(_Endereco));
@ -94,7 +102,7 @@ class SensorCorrente_v2 : public ComponenteCAN {
if (isINA219) {
sINA219 = Adafruit_INA219(_Endereco);
bool iniciou = sINA219.begin();
bool iniciou = sINA219.begin(&Wire);
AtualizarEstadoIniciado(iniciou);
if (iniciou) {
@ -106,7 +114,7 @@ class SensorCorrente_v2 : public ComponenteCAN {
}
else {
sINA228 = Adafruit_INA228();
bool iniciou = sINA228.begin(_Endereco);
bool iniciou = sINA228.begin(_Endereco, &Wire);
AtualizarEstadoIniciado(iniciou);
if (iniciou) {
@ -148,6 +156,7 @@ class SensorCorrente_v2 : public ComponenteCAN {
data.push_back(static_cast<uint8_t>(posicao));
data.push_back(ID_Num);
const uint32_t geracaoAtual = I2CService::GeracaoBarramento();
bool iniciado;
bool ina219;
float snapBusVoltage;
@ -159,7 +168,9 @@ class SensorCorrente_v2 : public ComponenteCAN {
// Copia curta e atômica. Serialização acontece fora da critical section.
portENTER_CRITICAL(&_snapshotLock);
iniciado = Iniciado;
iniciado = Iniciado &&
geracaoAtual != 0 &&
_geracaoI2CInicializacao == geracaoAtual;
ina219 = isINA219;
snapBusVoltage = busVoltage;
snapCurrent = current;
@ -210,11 +221,16 @@ class SensorCorrente_v2 : public ComponenteCAN {
portMUX_TYPE _snapshotLock = portMUX_INITIALIZER_UNLOCKED;
uint8_t _falhasConsecutivas = 0;
uint32_t _totalFalhasLeitura = 0;
uint32_t _geracaoI2CInicializacao = 0;
static constexpr uint8_t LimiteFalhasAntesReinit = 3;
void AtualizarEstadoIniciado(bool iniciado) {
const uint32_t geracao =
iniciado ? I2CService::GeracaoBarramento() : 0;
portENTER_CRITICAL(&_snapshotLock);
Iniciado = iniciado;
_geracaoI2CInicializacao = geracao;
portEXIT_CRITICAL(&_snapshotLock);
}
@ -238,12 +254,16 @@ class SensorCorrente_v2 : public ComponenteCAN {
AtualizarEstadoIniciado(false);
MostrarLog("Sensor marcado para reinicializacao controlada");
}
I2CService::NotificarFalhaDispositivo(ID_Num, motivo);
}
void RegistrarSucessoAquisicao() {
portENTER_CRITICAL(&_snapshotLock);
_falhasConsecutivas = 0;
portEXIT_CRITICAL(&_snapshotLock);
I2CService::NotificarSucessoDispositivo(ID_Num);
}
bool AmostraValida(
@ -307,7 +327,7 @@ class SensorCorrente_v2 : public ComponenteCAN {
// As APIs Adafruit abaixo nao propagam de forma util os erros de Wire.
// Este ping confirma o dispositivo no ramal antes da aquisicao.
if (!I2CService::VerificaEnderecoBarramento(_Endereco)) {
if (!I2CService::VerificaEnderecoBarramentoPassivo(_Endereco)) {
I2CService::LiberarAcessoI2C(ID_Num);
RegistrarFalhaAquisicao("dispositivo nao respondeu no endereco I2C");
return;
@ -341,12 +361,6 @@ class SensorCorrente_v2 : public ComponenteCAN {
);
if (!amostraValida) {
// Ainda dentro do ownership do I2C: o service consegue associar a
// falha ao ID/canal correto antes de o mutex ser liberado.
I2CService::NotificarFalhaDispositivo(
ID_Num,
"amostra INA invalida"
);
I2CService::LiberarAcessoI2C(ID_Num);
RegistrarFalhaAquisicao("amostra invalida/fora da faixa");
return;

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,7 @@ bool Conectado = false;
bool Conectar = false;
int _TaxaAmostragem = 1000;
bool RequisitarDados = false;
bool RequisitarDados = true;
int latenciaLoop = 0;
int qtdEnviosStatus = 0;
int maxEnviosStatus = 5;
@ -76,9 +76,9 @@ uint32_t publicacaoMaxMs = 0;
// Aquisição física é responsabilidade exclusiva da TelemetriaTask.
// O caminho CAN apenas serializa os snapshots já disponíveis em memória.
static constexpr uint32_t PERIODO_CORRENTE_MS = 200; // 5 Hz
static constexpr uint32_t PERIODO_NTC_MS = 500; // 2 Hz
static constexpr uint32_t PERIODO_RETRY_I2C_MS = 10000; // retry lento
static constexpr uint32_t PERIODO_CORRENTE_MS = 500; // 2 Hz
static constexpr uint32_t PERIODO_NTC_MS = 1000; // 1 Hz
static constexpr uint32_t PERIODO_RETRY_I2C_MS = 3000; // retry lento
static constexpr uint32_t PERIODO_RETRY_SENSOR_MS = 10000;
static constexpr uint32_t PERIODO_DIAG_TELEMETRIA_MS = 10000;
@ -139,10 +139,14 @@ Rele_v2* ObterRelePorId(uint8_t idNum) {
#pragma endregion
#pragma region TEMPERATURA NTC
SensorTemperaturaNtc sNTC_MotorET("TMVET", 1, 6, TiposBarramentos::CONTROLADOR, -1, 0.0f, false);
SensorTemperaturaNtc sNTC_MotorDT("TMVDT", 3, 7, TiposBarramentos::CONTROLADOR, -1, 0.0f, true);
SensorTemperaturaNtc sNTC_MotorEF("TMVEF", 2, 8, TiposBarramentos::CONTROLADOR, -1, 0.0f, false);
SensorTemperaturaNtc sNTC_MotorDF("TMVDF", 4, 9, TiposBarramentos::CONTROLADOR, -1, 0.0f, true);
//SensorTemperaturaNtc sNTC_MotorET("TMVET", 1, 6, TiposBarramentos::CONTROLADOR, -1, 0.0f, false);
SensorTemperaturaNtc sNTC_MotorET("TMVET", 1, 0, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true);
//SensorTemperaturaNtc sNTC_MotorDT("TMVDT", 3, 7, TiposBarramentos::CONTROLADOR, -1, 0.0f, true);
SensorTemperaturaNtc sNTC_MotorDT("TMVDT", 3, 1, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true);
//SensorTemperaturaNtc sNTC_MotorEF("TMVEF", 2, 8, TiposBarramentos::CONTROLADOR, -1, 0.0f, false);
SensorTemperaturaNtc sNTC_MotorEF("TMVEF", 2, 2, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true);
//SensorTemperaturaNtc sNTC_MotorDF("TMVDF", 4, 9, TiposBarramentos::CONTROLADOR, -1, 0.0f, true);
SensorTemperaturaNtc sNTC_MotorDF("TMVDF", 4, 3, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true);
SensorTemperaturaNtc* ObterTempNtcPorId(uint8_t idNum) {
switch (idNum) {
@ -195,6 +199,156 @@ SensorMagnetico_v2* ObterSensorMagneticoPorId(uint8_t idNum) {
}
#pragma endregion
#pragma region DIAGNOSTICO
uint8_t SaturarByte(uint32_t valor) {
return valor > 255 ? 255 : (uint8_t)valor;
}
uint16_t SaturarU16(uint32_t valor) {
return valor > 65535U ? 65535U : static_cast<uint16_t>(valor);
}
uint8_t MascaraSensoresCorrenteIniciados() {
uint8_t mask = 0;
SensorCorrente_v2* sensores[] = {
&sCOR_3V0,
&sCOR_5V0,
&sCOR_7V2,
&sCOR_12V,
&sCOR_19V,
&sCOR_24V,
&sCOR_36V
};
for (uint8_t i = 0; i < 7; i++) {
if (sensores[i] != nullptr && sensores[i]->EstaIniciado()) {
mask |= (uint8_t)(1U << i);
}
}
return mask;
}
std::vector<uint8_t> MontarFrameDiagnosticoI2C(CanMessagePosicaoDados posicao) {
std::vector<uint8_t> data;
data.push_back(static_cast<uint8_t>(posicao));
data.push_back(canService.ID_Num_sMOD);
I2CService::DiagnosticoCAN diag = I2CService::ObterDiagnosticoCAN();
if (posicao == CanMessagePosicaoDados::Dados1) {
uint8_t flagsEstado = 0;
if (diag.i2cIniciado)
flagsEstado |= (1U << 0);
if (diag.muxIniciado)
flagsEstado |= (1U << 1);
if (diag.infraestruturaMuxOk)
flagsEstado |= (1U << 2);
if (diag.recoveryEmAndamento)
flagsEstado |= (1U << 3);
if (diag.recoveryPendente)
flagsEstado |= (1U << 4);
if (diag.sdaHigh)
flagsEstado |= (1U << 5);
if (diag.sclHigh)
flagsEstado |= (1U << 6);
if (diag.ultimoIdFalha >= 0)
flagsEstado |= (1U << 7);
uint8_t flagsTca = 0;
if (diag.tcaRdyConfigurado)
flagsTca |= (1U << 0);
if (diag.tcaReady)
flagsTca |= (1U << 1);
if (diag.tcaEnConfigurado)
flagsTca |= (1U << 2);
if (diag.tcaEnHigh)
flagsTca |= (1U << 3);
if (diag.algumCanalMuxQuarentena)
flagsTca |= (1U << 4);
if (diag.algumAdsQuarentena)
flagsTca |= (1U << 5);
data.push_back(flagsEstado);
data.push_back(flagsTca);
data.push_back((uint8_t)(diag.geracaoBarramento & 0xFF));
data.push_back(SaturarByte(diag.totalRecoveries));
data.push_back(SaturarByte(diag.falhasRecoveryConsecutivas));
data.push_back(SaturarByte(diag.falhasTransacaoConsecutivas));
}
else if (posicao == CanMessagePosicaoDados::Dados2) {
data.push_back(diag.ultimoIdFalha >= 0 && diag.ultimoIdFalha <= 255 ? (uint8_t)diag.ultimoIdFalha : 0xFF);
data.push_back(diag.ultimoCanalFalha >= 0 && diag.ultimoCanalFalha <= 255 ? (uint8_t)diag.ultimoCanalFalha : 0xFF);
data.push_back(diag.muxEsperadoMask);
data.push_back(diag.muxValidadoMask);
data.push_back(diag.mux0QuarentenaMask);
data.push_back(MascaraSensoresCorrenteIniciados());
}
else if (posicao == CanMessagePosicaoDados::Dados3) {
const uint16_t maxAdsMs = SaturarU16(diag.maxLeituraAdsMs);
const uint16_t timeoutsAds = SaturarU16(diag.timeoutsAds);
const uint16_t readyDropsAds = SaturarU16(diag.readyDropsAds);
// bytes 2..3
data.push_back(static_cast<uint8_t>(maxAdsMs >> 8));
data.push_back(static_cast<uint8_t>(maxAdsMs & 0xFF));
// bytes 4..5
data.push_back(static_cast<uint8_t>(timeoutsAds >> 8));
data.push_back(static_cast<uint8_t>(timeoutsAds & 0xFF));
// bytes 6..7
data.push_back(static_cast<uint8_t>(readyDropsAds >> 8));
data.push_back(static_cast<uint8_t>(readyDropsAds & 0xFF));
}
else if (posicao == CanMessagePosicaoDados::Dados4) {
const uint16_t maxStart = SaturarU16(diag.maxInicioAdsMs);
const uint16_t maxRead = SaturarU16(diag.maxResultadoAdsMs);
const uint16_t falhasIo = SaturarU16(diag.falhasIoAds);
data.push_back((uint8_t)(maxStart >> 8));
data.push_back((uint8_t)(maxStart & 0xFF));
data.push_back((uint8_t)(maxRead >> 8));
data.push_back((uint8_t)(maxRead & 0xFF));
data.push_back((uint8_t)(falhasIo >> 8));
data.push_back((uint8_t)(falhasIo & 0xFF));
}
return data;
}
#pragma endregion
void MostrarLog(String mensagem) {
bool _debugMode = false;
if (_debugMode) {
@ -203,9 +357,9 @@ void MostrarLog(String mensagem) {
}
void inicializarDependenciasI2C() {
I2CService::DefinirPinos(1, 2, -1, -1, 39, 40);
I2CService::DefinirPinos(1, 2, 10, 11, 39, 40);
I2CService::IniciarI2C();
//I2CService::IniciarADS();
I2CService::IniciarADS();
I2CService::IniciarMUX();
//I2CService::IniciarMCP();
}
@ -260,6 +414,30 @@ void TentarReinicializarSensoresCorrente() {
}
}
void TentarReinicializarSensoresNtc() {
if (!I2CService::I2CIniciado)
return;
if (!I2CService::AdsIniciado) {
if (!I2CService::IniciarADS())
return;
}
SensorTemperaturaNtc* sensores[] = {
&sNTC_MotorET,
&sNTC_MotorDT,
&sNTC_MotorEF,
&sNTC_MotorDF
};
for (auto* sensor : sensores) {
if (sensor != nullptr && !sensor->EstaIniciado()) {
sensor->Inicializar();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
void TelemetriaTask(void* pv) {
TickType_t ultimaCorrente = 0;
TickType_t ultimoNtc = 0;
@ -271,18 +449,18 @@ void TelemetriaTask(void* pv) {
const TickType_t agora = xTaskGetTickCount();
if ((agora - ultimaCorrente) >= pdMS_TO_TICKS(PERIODO_CORRENTE_MS)) {
ultimaCorrente = agora;
const uint32_t inicioAquisicao = millis();
AtualizarSensoresCorrente();
ultimaCorrente = xTaskGetTickCount();
const uint32_t duracao = millis() - inicioAquisicao;
if (duracao > maxAquisicaoCorrenteMs) maxAquisicaoCorrenteMs = duracao;
ciclosTelemetria++;
}
if ((agora - ultimoNtc) >= pdMS_TO_TICKS(PERIODO_NTC_MS)) {
ultimoNtc = agora;
const uint32_t inicioAquisicao = millis();
AtualizarSensoresNtc();
ultimoNtc = xTaskGetTickCount();
const uint32_t duracao = millis() - inicioAquisicao;
if (duracao > maxAquisicaoNtcMs) maxAquisicaoNtcMs = duracao;
}
@ -295,6 +473,7 @@ void TelemetriaTask(void* pv) {
if ((agora - ultimoRetrySensor) >= pdMS_TO_TICKS(PERIODO_RETRY_SENSOR_MS)) {
ultimoRetrySensor = agora;
TentarReinicializarSensoresCorrente();
TentarReinicializarSensoresNtc();
}
if ((agora - ultimoDiag) >= pdMS_TO_TICKS(PERIODO_DIAG_TELEMETRIA_MS)) {
@ -342,21 +521,33 @@ void setup() {
sLED_Manual.Inicializar();
sLED_Automatico.Inicializar();
sLED_Operacao.Inicializar();
sNTC_MotorET.Inicializar();
sNTC_MotorDT.Inicializar();
sNTC_MotorEF.Inicializar();
sNTC_MotorDF.Inicializar();
sCOR_3V0.Inicializar();
sCOR_5V0.Inicializar();
sCOR_7V2.Inicializar();
sCOR_12V.Inicializar();
sCOR_19V.Inicializar();
sCOR_24V.Inicializar();
sCOR_36V.Inicializar();
//sMAG_MotorET.Inicializar();
//sMAG_MotorDT.Inicializar();
//sMAG_MotorEF.Inicializar();
//sMAG_MotorDF.Inicializar();
if (I2CService::I2CIniciado) {
if (I2CService::AdsIniciado) {
sNTC_MotorET.Inicializar();
sNTC_MotorDT.Inicializar();
sNTC_MotorEF.Inicializar();
sNTC_MotorDF.Inicializar();
}
if (I2CService::MuxIniciado) {
sCOR_3V0.Inicializar();
sCOR_5V0.Inicializar();
sCOR_7V2.Inicializar();
sCOR_12V.Inicializar();
sCOR_19V.Inicializar();
sCOR_24V.Inicializar();
sCOR_36V.Inicializar();
}
//sMAG_MotorET.Inicializar();
//sMAG_MotorDT.Inicializar();
//sMAG_MotorEF.Inicializar();
//sMAG_MotorDF.Inicializar();
}
ledManager.Adicionar(&sLED_Manual);
ledManager.Adicionar(&sLED_Automatico);
@ -439,9 +630,33 @@ void enviarDadosSensores(uint8_t id_num, CanMessagePosicaoDados posicao) {
bool todosIDs = id_num == canService.ID_Num_sTOD;
if (id_num == canService.ID_Num_sMOD) {
EnviarDadosCAN(canService.MontarFrameReqStatusMod(D_Code, cfg.conectado, VERSION));
if (PublicacaoMutex != nullptr) xSemaphoreGive(PublicacaoMutex);
return;
switch (posicao) {
case CanMessagePosicaoDados::Status:
EnviarDadosCAN(canService.MontarFrameReqStatusMod(D_Code, cfg.conectado, VERSION));
break;
case CanMessagePosicaoDados::Dados1:
case CanMessagePosicaoDados::Dados2:
case CanMessagePosicaoDados::Dados3:
case CanMessagePosicaoDados::Dados4:
EnviarDadosCAN(MontarFrameDiagnosticoI2C(posicao));
break;
default:
break;
}
if (PublicacaoMutex != nullptr)
xSemaphoreGive(PublicacaoMutex);
return;
}
if (todosIDs && todosDados) {
EnviarDadosCAN(MontarFrameDiagnosticoI2C(CanMessagePosicaoDados::Dados1));
EnviarDadosCAN(MontarFrameDiagnosticoI2C(CanMessagePosicaoDados::Dados2));
EnviarDadosCAN(MontarFrameDiagnosticoI2C(CanMessagePosicaoDados::Dados3));
EnviarDadosCAN(MontarFrameDiagnosticoI2C(CanMessagePosicaoDados::Dados4));
}
if (todosIDs)