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

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@ class ModuloAtuador(ModuloDiagnosticoBase):
self.padroes_bicos = {} self.padroes_bicos = {}
self.padroes_bombas = {} self.padroes_bombas = {}
self.runtime_bombas = {} self.runtime_bombas = {}
self.runtime_bicos = {}
def atualizar_saude(self): def atualizar_saude(self):
try: try:
@ -612,50 +613,149 @@ class ModuloAtuador(ModuloDiagnosticoBase):
dados = item["dados"] dados = item["dados"]
label = dados.get("label") label = dados.get("label")
agora = time.monotonic()
runtime = self._runtime_bico(item)
# ============================================================
# Comando desejado e estado físico
# ============================================================
comando_presente = ( comando_presente = (
"comando_atuar" in dados or "comando_atuar" in dados
"estado_desejado" in dados or or "estado_desejado" in dados
"status_desejado_fisico" in dados or or "status_desejado_fisico" in dados
"ultimo_status_desejado" in dados or "ultimo_status_desejado" in dados
) )
comando = self._estado_fisico_desejado_ligado(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) 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 = None
angulo_desejado = None angulo_desejado = None
angulo_neutro = None angulo_neutro = None
angulo_controle = None angulo_controle = None
erro_angulo = 0.0
if usa_servo: if usa_servo:
angulo = self._float( 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, 0,
) )
angulo_neutro = self._float(dados.get("angulo_neutro", 90), 90) angulo_neutro = self._float(
angulo_controle = self._float(dados.get("angulo_controle", dados.get("angulo_sp", 90)), 90) dados.get("angulo_neutro", 90),
90,
)
angulo_controle = self._float(
dados.get(
"angulo_controle",
dados.get("angulo_sp", 90),
),
90,
)
angulo_desejado = self._float( angulo_desejado = self._float(
dados.get( dados.get(
"angulo_desejado_fisico", "angulo_desejado_fisico",
dados.get( dados.get(
"angulo_desejado", "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) erro_angulo = abs(
tem_idade_comando = ultimo_comando_ms < 900000 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: 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, { return 92, {
"comando_disponivel": False, "comando_disponivel": False,
"estado_on": estado_on, "estado_on": estado_on,
@ -664,104 +764,368 @@ class ModuloAtuador(ModuloDiagnosticoBase):
"angulo_desejado": angulo_desejado, "angulo_desejado": angulo_desejado,
} }
if tem_idade_comando and ultimo_comando_ms < self.COMANDO_GRACE_MS: # ============================================================
return 96, { # Telemetria velha
"comando_atuar": comando, #
"estado_on": estado_on, # A avaliação de resposta do componente já diagnosticará isso.
"usa_servo": usa_servo, # Não devemos transformar uma leitura antiga em falha mecânica.
"angulo": angulo, # ============================================================
"angulo_desejado": angulo_desejado,
"ultimo_comando_ms": ultimo_comando_ms,
}
if self._aguardando_telemetria_pos_comando(dados): if not telemetria_fresca:
return 94, { self._atualizar_persistencia(
"comando_disponivel": True, runtime,
"aguardando_telemetria_pos_comando": True, "comandado_on_leitura_off_desde",
"comando_fisico": comando, False,
"comando_logico": self._comando_logico_ligado(dados), agora,
"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)
) )
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})") self._atualizar_persistencia(
condicoes.append({ runtime,
"label": "Bico", "angulo_divergente_desde",
"valor": 1, False,
"severidade": severidade, agora,
"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.",
],
})
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): divergencia_ligar = (
motivos.append(f"Ângulo do bico distante do desejado ({angulo:.1f}°/{angulo_desejado:.1f}°)") comando and not estado_on
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)
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_disponivel": True,
"comando_atuar": comando, "comando_fisico": comando,
"comando_logico": self._comando_logico_ligado(dados),
"estado_on": estado_on, "estado_on": estado_on,
"usa_servo": usa_servo, "usa_servo": usa_servo,
"angulo": angulo, "angulo": angulo,
"angulo_desejado": angulo_desejado, "angulo_desejado": angulo_desejado,
"angulo_neutro": angulo_neutro, "angulo_neutro": angulo_neutro,
"angulo_controle": angulo_controle, "angulo_controle": angulo_controle,
"erro_angulo": erro_angulo,
"ultimo_comando_ms": ultimo_comando_ms, "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 # Bombas
# ============================================================ # ============================================================
@ -1369,6 +1733,22 @@ class ModuloAtuador(ModuloDiagnosticoBase):
return self.runtime_bombas[key] 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 @staticmethod
def _atualizar_persistencia(runtime, campo, condicao, agora): def _atualizar_persistencia(runtime, campo, condicao, agora):
""" """

View File

@ -1,4 +1,5 @@
import time import time
import math
from shared.enums import T_Code, StatusModulo from shared.enums import T_Code, StatusModulo
from shared.utils import penalizar_frequencia from shared.utils import penalizar_frequencia
from health_worker.modulos.base import ModuloDiagnosticoBase from health_worker.modulos.base import ModuloDiagnosticoBase
@ -56,10 +57,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
operacao = ContextoGlobalRedis.get_operacao() or {} operacao = ContextoGlobalRedis.get_operacao() or {}
equipamento = ContextoGlobalRedis.get_equipamento() 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) 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") sensores = self._coletar(dados_sen, "sensores")
servos = self._coletar(dados_sen, "servos") servos = self._coletar(dados_sen, "servos")
@ -78,7 +81,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
updates = {} updates = {}
for item in sensores: for item in sensores:
s = self._avaliar_sensor(item, ctx) s = self._avaliar_sensor(item, ctx, modulo_disponivel=sen_disponivel)
saude_individual.append(s) saude_individual.append(s)
updates[f"sensores.{item['key']}.saude"] = s updates[f"sensores.{item['key']}.saude"] = s
@ -126,6 +129,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
if not conectado: if not conectado:
saude_final = 0 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"]): if any(self._is_falha_hardware(c) for c in root_cond + processo["condicoes_operacionais"]):
saude_final = min(saude_final, 40) saude_final = min(saude_final, 40)
if processo.get("energia_critica", False): if processo.get("energia_critica", False):
@ -156,6 +161,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
saude_geral = { saude_geral = {
"conectado": conectado, "conectado": conectado,
"telemetria_recente": conectado,
"firmware_conectado": firmware_conectado,
"status": status.value, "status": status.value,
"saude": saude_final, "saude": saude_final,
"motivos": self._dedupe(motivos), "motivos": self._dedupe(motivos),
@ -196,7 +203,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
# ============================================================ # ============================================================
def _avaliar_root(self, dados_sen): 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) idade_ms = self._ultima_resposta_ms(dados_sen)
motivos = [] motivos = []
condicoes = [] condicoes = []
@ -275,22 +287,97 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
# Sensores passivos # Sensores passivos
# ============================================================ # ============================================================
def _avaliar_sensor(self, item, ctx): def _avaliar_sensor(self, item, ctx, modulo_disponivel=True):
dados = item["dados"] dados = item["dados"]
conectado = self._bool(dados.get("conectado", False)) conectado = self._bool(dados.get("conectado", False))
aferir = self._bool(dados.get("aferir", False)) aferir = self._bool(dados.get("aferir", False))
mandatorio = self._bool(dados.get("mandatorio", False)) mandatorio = self._bool(dados.get("mandatorio", False))
delegado_ao_mov = str(dados.get("label", "")).upper().startswith("TMV") delegado_ao_mov = str(dados.get("label", "")).upper().startswith("TMV")
em_uso = aferir and not delegado_ao_mov 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 = [] motivos = []
condicoes = [] 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") resposta_score = self._score_resposta(dados, motivos, condicoes, "Sensor SEN")
valor_score, valor_debug = self._avaliar_valores_sensor(item, ctx, motivos, condicoes) criticos = self._labels_criticos_sensor(dados, ctx, mandatorio)
telemetria = self._avaliar_telemetria(dados, motivos, 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 saude = resposta_score * 0.35 + valor_score * 0.45 + telemetria["score"] * 0.20
if any(self._is_falha_hardware(c) for c in condicoes): 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)))) saude = int(max(0, min(100, round(saude))))
status = self._resolver_status_item(conectado, saude, condicoes) 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"] dados = item["dados"]
sensor_label = str(dados.get("label", "")) sensor_label = str(dados.get("label", ""))
criticos = {self._canon(c) for c in (criticos or []) if c}
score = 100.0 score = 100.0
debug = {} debug = {}
for key, campo in self._campos(dados).items(): for key, campo in self._campos(dados).items():
label = str(campo.get("label") or key) 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) minimo = campo.get("minimo", None)
maximo = campo.get("maximo", None) maximo = campo.get("maximo", None)
nominal = campo.get("nominal", None) nominal = campo.get("nominal", None)
unidade = campo.get("unidade", campo.get("unidade_medida", "")) unidade = campo.get("unidade", campo.get("unidade_medida", ""))
debug[key] = {"label": label, "atual": valor, "minimo": minimo, "maximo": maximo, "nominal": nominal, "unidade": unidade} 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) sc, conds, mots = self._avaliar_campo(sensor_label, label, valor, minimo, maximo)
score = min(score, sc) score = min(score, sc)
motivos.extend(mots) motivos.extend(mots)
@ -509,6 +629,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
token_desejado = round(angulo_sp, 3) token_desejado = round(angulo_sp, 3)
self._registrar_desejado(runtime, token_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"]
erro = abs(angulo - angulo_sp) erro = abs(angulo - angulo_sp)
debug["erro"] = erro debug["erro"] = erro
@ -532,12 +657,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
debug["avaliacao_suspensa"] = "aguardando primeiro comando" debug["avaliacao_suspensa"] = "aguardando primeiro comando"
return 96, debug return 96, debug
if not evidencia["telemetria_pos_comando"]: if not runtime["telemetria_confirmada_desejo"]:
runtime["divergencia_desde"] = None runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando" debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando"
return 96, debug return 96, debug
if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS: if idade_desejo_ms < self.COMANDO_GRACE_MS:
runtime["divergencia_desde"] = None 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 return 96, debug
@ -862,6 +987,9 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
"coerencia_desde": None, "coerencia_desde": None,
"nivel_divergencia": None, "nivel_divergencia": None,
"ultimo_desejado": None, "ultimo_desejado": None,
"desejo_alterado_em": None,
"telemetria_confirmada_desejo": False,
"possui_comando_anterior": False,
}) })
def _preparar_atuador_auxiliar( def _preparar_atuador_auxiliar(
@ -995,10 +1123,18 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
def _registrar_desejado(self, runtime, token_desejado): def _registrar_desejado(self, runtime, token_desejado):
if runtime.get("ultimo_desejado") == 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["ultimo_desejado"] = token_desejado
runtime["desejo_alterado_em"] = time.monotonic()
runtime["telemetria_confirmada_desejo"] = False
runtime["possui_comando_anterior"] = False
return True
@staticmethod @staticmethod
def _resetar_estado_comando(runtime, manter_desejado=True): def _resetar_estado_comando(runtime, manter_desejado=True):
@ -1007,6 +1143,43 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
runtime["nivel_divergencia"] = None runtime["nivel_divergencia"] = None
if not manter_desejado: if not manter_desejado:
runtime["ultimo_desejado"] = None 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( def _avaliar_comando_binario(
self, self,
@ -1019,10 +1192,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
): ):
dados = item["dados"] dados = item["dados"]
label = dados.get("label") label = dados.get("label")
controlar = self._bool(dados.get("controlar", False))
controlar = self._bool(
dados.get("controlar", False)
)
desejado_presente = any( desejado_presente = any(
chave in dados chave in dados
@ -1051,89 +1221,95 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
lido = self._estado_ligado( lido = self._estado_ligado(
dados.get( dados.get(
"estado_leitura", "estado_leitura",
self._campo_atual( self._campo_atual(dados, "EstadoLeitura", False),
dados,
"EstadoLeitura",
False,
),
) )
) )
ultimo_comando_ms = self._float( evidencia = self._evidencia_comando(dados)
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
)
debug = { debug = {
"controlar": controlar, "controlar": controlar,
"estado_lido": lido, "estado_lido": lido,
"estado_desejado": desejado, "estado_desejado": desejado,
"possui_comando": possui_comando, **evidencia,
"telemetria_pos_comando": telemetria_pos_comando,
"ultimo_comando_ms": ultimo_comando_ms,
"ultima_resposta_ms": ultima_resposta_ms,
} }
if not controlar: if not controlar:
runtime["divergencia_desde"] = None self._resetar_estado_comando(runtime, manter_desejado=False)
return 100, debug return 100, debug
if not desejado_presente or not leitura_presente: 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 debug["contrato_comando_completo"] = False
return 92, debug 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: if lido == desejado:
runtime["divergencia_desde"] = None runtime["divergencia_desde"] = None
debug["divergente"] = False 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 debug["divergente"] = True
runtime["coerencia_desde"] = None
# Não existe evidência de que o estado desejado foi enviado. if not evidencia["possui_comando"]:
if not possui_comando:
runtime["divergencia_desde"] = None runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando primeiro comando" debug["avaliacao_suspensa"] = "aguardando primeiro comando"
return 96, debug return 96, debug
# A resposta ainda é antiga em relação ao comando. if not runtime["telemetria_confirmada_desejo"]:
if not telemetria_pos_comando:
runtime["divergencia_desde"] = None runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = ( debug["avaliacao_suspensa"] = (
"aguardando telemetria posterior ao comando" "aguardando telemetria posterior ao comando"
) )
return 96, debug return 96, debug
# Comando acabou de ser enviado. if idade_desejo_ms < self.COMANDO_GRACE_MS:
if ultimo_comando_ms < self.COMANDO_GRACE_MS:
runtime["divergencia_desde"] = None runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = ( debug["avaliacao_suspensa"] = "período de resposta ao comando"
"período de resposta ao comando"
)
return 96, debug return 96, debug
agora = time.monotonic() agora = time.monotonic()
@ -1147,16 +1323,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
< self.COMPONENTE_RECUPERACAO_GRACE_MS < self.COMPONENTE_RECUPERACAO_GRACE_MS
): ):
runtime["divergencia_desde"] = None runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = ( debug["avaliacao_suspensa"] = "estabilização após reconexão"
"estabilização após reconexão"
)
debug["tempo_desde_retorno_componente_ms"] = ( debug["tempo_desde_retorno_componente_ms"] = (
tempo_desde_retorno_componente_ms tempo_desde_retorno_componente_ms
) )
return 96, debug 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: if runtime["divergencia_desde"] is None:
runtime["divergencia_desde"] = agora runtime["divergencia_desde"] = agora
@ -1166,39 +1338,54 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
debug["tempo_divergente_ms"] = tempo_divergente_ms 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: if tempo_divergente_ms < self.COMANDO_ALERTA_MS:
return 96, debug return 96, debug
if tempo_divergente_ms < self.COMANDO_FALHA_MS: if tempo_divergente_ms < self.COMANDO_FALHA_MS:
runtime["nivel_divergencia"] = "alerta"
motivos.append( motivos.append(
f"{label} ainda não acompanhou o estado desejado" f"{label} ainda não acompanhou o estado desejado"
) )
condicoes.append({ condicoes.append({
"label": nome, "label": nome,
"valor": int(lido), "valor": int(lido),
"severidade": 78, "severidade": 78,
"classe": "processo", "classe": "processo",
"descricao": "descricao": (
f"{label} apresenta divergência persistente de comando", f"{label} apresenta divergência persistente de comando"
),
"acoes": [ "acoes": [
"Aguardar confirmação ou verificar saída física.", "Aguardar confirmação ou verificar saída física.",
], ],
}) })
return 68, debug return 68, debug
motivos.append( runtime["nivel_divergencia"] = "falha"
f"{label} não acompanhou o estado desejado"
)
motivos.append(f"{label} não acompanhou o estado desejado")
condicoes.append({ condicoes.append({
"label": nome, "label": nome,
"valor": int(lido), "valor": int(lido),
"severidade": severidade_falha, "severidade": severidade_falha,
"classe": "falha_hardware", "classe": "falha_hardware",
"descricao": "descricao": f"{label} permaneceu diferente do estado desejado",
f"{label} permaneceu diferente do estado desejado",
"acoes": [ "acoes": [
"Verificar saída física do relé.", "Verificar saída física do relé.",
"Verificar carga conectada.", "Verificar carga conectada.",
@ -1319,6 +1506,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
st_sp if status_sp_presente else None, st_sp if status_sp_presente else None,
) )
self._registrar_desejado(runtime, token_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"]
# Se o SP existe, uma leitura ausente (-1) também é divergência. # Se o SP existe, uma leitura ausente (-1) também é divergência.
coerente_id = ( coerente_id = (
@ -1366,12 +1558,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
debug["avaliacao_suspensa"] = "aguardando primeiro comando" debug["avaliacao_suspensa"] = "aguardando primeiro comando"
return 96, debug return 96, debug
if not evidencia["telemetria_pos_comando"]: if not runtime["telemetria_confirmada_desejo"]:
runtime["divergencia_desde"] = None runtime["divergencia_desde"] = None
debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando" debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando"
return 96, debug return 96, debug
if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS: if idade_desejo_ms < self.COMANDO_GRACE_MS:
runtime["divergencia_desde"] = None 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 return 96, debug
@ -1653,6 +1845,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
if not conectado: if not conectado:
return StatusModulo.DESCONECTADO return StatusModulo.DESCONECTADO
em_uso = [x for x in saude_individual if self._bool(x.get("em_uso", False))] 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): if any(self._is_falha_hardware(c) for c in condicoes):
return StatusModulo.FALHA return StatusModulo.FALHA
if any(x.get("status") == StatusModulo.FALHA.value for x in em_uso): if any(x.get("status") == StatusModulo.FALHA.value for x in em_uso):
@ -1665,6 +1859,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
return StatusModulo.ALERTA return StatusModulo.ALERTA
if any(x.get("status") == StatusModulo.ALERTA.value for x in em_uso): if any(x.get("status") == StatusModulo.ALERTA.value for x in em_uso):
return StatusModulo.ALERTA return StatusModulo.ALERTA
if any(x.get("status") == StatusModulo.DESCONECTADO.value for x in em_uso):
return StatusModulo.ALERTA
return StatusModulo.OPERANTE return StatusModulo.OPERANTE
@staticmethod @staticmethod
@ -1795,14 +1991,24 @@ class ModuloSensoriamento(ModuloDiagnosticoBase):
@staticmethod @staticmethod
def _float(valor, padrao=0.0): def _float(valor, padrao=0.0):
fallback = None if padrao is None else float(padrao)
try: try:
if valor is None: if valor is None:
return float(padrao) return fallback
if hasattr(valor, "value"): if hasattr(valor, "value"):
valor = valor.value valor = valor.value
return float(valor)
numero = float(valor)
if not math.isfinite(numero):
return fallback
return numero
except Exception: except Exception:
return float(padrao) return fallback
@staticmethod @staticmethod
def _safe_int(valor, padrao=0): 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 // 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. // realmente travado. Ele nao e usado para decidir se o barramento esta vivo.
esp_task_wdt_init(TASK_WDT_SECONDS, true); 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(CanTaskRxHandle);
esp_task_wdt_add(CanTaskTxHandle); esp_task_wdt_add(CanTaskTxHandle);
esp_task_wdt_add(CallbackWorkerHandle); esp_task_wdt_add(CallbackWorkerHandle);

View File

@ -76,6 +76,11 @@ class I2CService {
uint32_t readyDropsAds = 0; uint32_t readyDropsAds = 0;
uint32_t falhasIoAds = 0; uint32_t falhasIoAds = 0;
uint32_t wireTimeoutMs = 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() { static DiagnosticoCAN ObterDiagnosticoCAN() {
@ -121,6 +126,10 @@ class I2CService {
d.readyDropsAds = _readyDropsAds; d.readyDropsAds = _readyDropsAds;
d.falhasIoAds = _falhasIoAds; d.falhasIoAds = _falhasIoAds;
d.recoveriesLocaisMux = _recoveriesLocaisMux;
d.recoveriesLocaisAds = _recoveriesLocaisAds;
d.falhasLocaisDispositivos = _falhasLocaisDispositivos;
portEXIT_CRITICAL(&_estadoLock); portEXIT_CRITICAL(&_estadoLock);
d.infraestruturaMuxOk = (d.muxEsperadoMask & d.muxValidadoMask) == d.muxEsperadoMask; d.infraestruturaMuxOk = (d.muxEsperadoMask & d.muxValidadoMask) == d.muxEsperadoMask;
@ -390,8 +399,58 @@ class I2CService {
return RecuperarBarramento(motivo, ignorarCooldown); 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) { 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) { static bool CanalMuxEmQuarentena(uint8_t canalGlobal) {
@ -578,6 +637,9 @@ class I2CService {
s += " SCL=" + String((_pinoSCL >= 0) ? digitalRead(_pinoSCL) : -1); s += " SCL=" + String((_pinoSCL >= 0) ? digitalRead(_pinoSCL) : -1);
s += " q_mux=" + String(QuantidadeCanaisMuxEmQuarentena()); s += " q_mux=" + String(QuantidadeCanaisMuxEmQuarentena());
s += " q_ads=" + String(QuantidadeADSEmQuarentena()); 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) { if (_pinoTcaRdy >= 0) {
s += " tca_rdy=" + String(digitalRead(_pinoTcaRdy) == HIGH ? 1 : 0); s += " tca_rdy=" + String(digitalRead(_pinoTcaRdy) == HIGH ? 1 : 0);
@ -761,6 +823,7 @@ class I2CService {
ContextoBarramento contexto = _contextoAtual; ContextoBarramento contexto = _contextoAtual;
if ( if (
_desabilitarMuxAoLiberar &&
contexto.valido && contexto.valido &&
contexto.canalMuxSolicitado >= 0 && contexto.canalMuxSolicitado >= 0 &&
!RecoveryPendente() !RecoveryPendente()
@ -944,65 +1007,240 @@ class I2CService {
return TrocarCanalMux(enderecoMux + indiceMux, canal); return TrocarCanalMux(enderecoMux + indiceMux, canal);
} }
static bool TrocarCanalMux(uint8_t endereco, uint8_t canalMux) { static bool EscreverMascaraMuxSemRegistrar(
int indiceMux = IndiceMux(endereco); uint8_t endereco,
uint8_t mascara,
uint8_t& erro,
uint32_t& duracaoMs
) {
const uint32_t inicio = millis();
if (indiceMux < 0 || canalMux >= 8) { Wire.beginTransmission(endereco);
MostrarLog( Wire.write(mascara);
"MUX", erro = Wire.endTransmission();
"Endereco ou canal invalido | endereco=0x" +
String(endereco, HEX) + " canal=" + String(canalMux) 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; return false;
} }
portENTER_CRITICAL(&_estadoLock); portENTER_CRITICAL(&_estadoLock);
_muxEsperadoMask |= (uint8_t)(1U << indiceMux); if (_recoveriesLocaisMux < UINT32_MAX) {
_recoveriesLocaisMux++;
}
portEXIT_CRITICAL(&_estadoLock); 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(); if (!ResetarMuxSePossivel()) {
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));
return false; return false;
} }
if (!_muxValidado[indiceMux]) { if (!TCAReadyOk()) {
MostrarLog("MUX", "Validando MUX em 0x" + String(endereco, HEX)); return false;
}
if (!VerificaEnderecoBarramento(endereco)) { if (
MostrarLog("MUX", "MUX nao encontrado em 0x" + String(endereco, HEX)); !PingEnderecoSemRegistrar(
return false; 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; _muxValidado[indiceMux] = true;
MuxIniciado = true; MuxIniciado = true;
} }
if (enderecoMuxAtivo == endereco && canalAtivo == canalMux) { if (
enderecoMuxAtivo == endereco &&
canalAtivo == canalMux
) {
return true; return true;
} }
// Evita deixar dois ramais conectados simultaneamente. /*
if (enderecoMuxAtivo != 0xFF && enderecoMuxAtivo != endereco) { * SEN atual possui um unico TCA9548A. Este bloco preserva suporte a
Wire.beginTransmission(enderecoMuxAtivo); * multiplos MUXes: se um endereco diferente ficou selecionado,
Wire.write(0x00); * desabilita-o antes de ativar o proximo.
int erroDesabilitar = Wire.endTransmission(); */
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( MostrarLog(
"MUX", "MUX",
"Falha ao desabilitar MUX anterior 0x" + "Falha ao desabilitar MUX anterior"
" | endereco=0x" +
String(enderecoMuxAtivo, HEX) + 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; return false;
} }
@ -1010,51 +1248,84 @@ class I2CService {
canalAtivo = -1; canalAtivo = -1;
} }
Wire.beginTransmission(endereco); const int canalAnterior =
Wire.write((uint8_t)(1U << canalMux)); CanalGlobalAtivoAtual();
int erro = Wire.endTransmission();
uint32_t duracao = (uint32_t)(millis() - inicio); uint8_t ultimoErro = 0xFF;
int sdaDepois = digitalRead(_pinoSDA); uint32_t ultimaDuracao = 0;
int sclDepois = digitalRead(_pinoSCL);
int rdyDepois = TCAReadyOk() ? 1 : 0;
MostrarLog( for (
"MUX", uint8_t tentativa = 0;
"Troca" tentativa <
" | anterior=" + String(canalAnterior) + TentativasTrocaMuxAntesResetLocal;
" solicitado=" + String(canalGlobal) + tentativa++
" erro=" + String(erro) + ) {
" duracao=" + String(duracao) + "ms" + const bool ok =
" antes[SDA=" + String(sdaAntes) + EscreverMascaraMuxSemRegistrar(
" SCL=" + String(sclAntes) + endereco,
" RDY=" + String(rdyAntes) + "]" + (uint8_t)(1U << canalMux),
" depois[SDA=" + String(sdaDepois) + ultimoErro,
" SCL=" + String(sclDepois) + ultimaDuracao
" RDY=" + String(rdyDepois) + "]" );
);
if (ok) {
enderecoMuxAtivo = endereco;
canalAtivo = canalMux;
RegistrarSucessoTransacao();
return true;
}
if (erro != 0) {
MostrarLog( MostrarLog(
"MUX", "MUX",
"Falha ao trocar canal | endereco=0x" + String(endereco, HEX) + "Falha local ao selecionar canal"
" canal=" + String(canalMux) + " erro=" + String(erro) " | 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; * As tentativas simples falharam. Antes de tocar no Wire, recupera
RegistrarSucessoTransacao(); * SOMENTE o MUX e tenta novamente.
*/
if (
RecuperarMuxLocalSemGeracao(
endereco,
canalMux
)
) {
RegistrarSucessoTransacao();
return true;
}
MostrarLog( /*
"MUX", * Somente agora uma falha do MUX alimenta o contador GLOBAL.
"Canal ativo | endereco=0x" + String(endereco, HEX) + * Sao necessarias LimiteFalhasTransacaoRecovery ocorrencias
" canal=" + String(canalMux) * 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) { static int SelecionarCanalADS(uint8_t canalGlobal) {
@ -1106,6 +1377,11 @@ class I2CService {
const bool leituraOk = LerADSSingleShotComTimeout(canal, leituraADC); const bool leituraOk = LerADSSingleShotComTimeout(canal, leituraADC);
if (!leituraOk) { if (!leituraOk) {
TratarFalhaADSLocal(
idSensor,
"falha de transporte/conversao"
);
leituraFalhou = true; leituraFalhou = true;
break; break;
} }
@ -1127,6 +1403,14 @@ class I2CService {
leiturasConcluidas++; leiturasConcluidas++;
} }
if (
!leituraFalhou &&
leiturasConcluidas == leituras
) {
ResetarFalhasAdsLocais();
RegistrarSucessoLocalDispositivo(idSensor);
}
if (RecoveryPendente()) { if (RecoveryPendente()) {
leituraFalhou = true; leituraFalhou = true;
} }
@ -1323,9 +1607,10 @@ class I2CService {
static ContextoBarramento _contextoFalha; static ContextoBarramento _contextoFalha;
static portMUX_TYPE _estadoLock; static portMUX_TYPE _estadoLock;
// Transacoes deste SEN sao de poucos bytes a 100 kHz. // No core 2.0.11 existem caminhos do Wire que podem ignorar este
// 25 ms ainda e uma margem muito grande, mas limita melhor uma // limite e retornar apenas ~1 s depois. Mantemos 25 ms como intencao do
// transferencia realmente emperrada. // driver, mas o recovery local evita transformar esse stall em reboot
// global imediato.
static constexpr uint32_t TimeoutWireMs = 25; static constexpr uint32_t TimeoutWireMs = 25;
static constexpr uint32_t LimiteReadyLowMs = 200; static constexpr uint32_t LimiteReadyLowMs = 200;
static constexpr uint32_t CooldownRecoveryMs = 1000; static constexpr uint32_t CooldownRecoveryMs = 1000;
@ -1350,6 +1635,24 @@ class I2CService {
static uint32_t _readyDropsAds; static uint32_t _readyDropsAds;
static uint32_t _falhasIoAds; 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) { static void MostrarLog(const String& componente, const String& mensagem) {
if (DebugMode) { if (DebugMode) {
PrintTela("[" + componente + "] " + mensagem); PrintTela("[" + componente + "] " + mensagem);
@ -1975,6 +2278,195 @@ class I2CService {
return total; 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) { static void RegistrarFalhaTransacao(const String& motivo) {
uint32_t falhas; uint32_t falhas;
@ -2455,7 +2947,13 @@ class I2CService {
static void RegistrarTimeoutADS(uint32_t inicioMs, const char* motivo) { static void RegistrarTimeoutADS(uint32_t inicioMs, const char* motivo) {
AtualizarMaxDuracaoADS(inicioMs); AtualizarMaxDuracaoADS(inicioMs);
IncrementarTimeoutADS(); IncrementarTimeoutADS();
RegistrarFalhaTransacao(motivo);
// Timeout isolado do ADS e degradacao LOCAL.
MostrarLog(
"ADS",
String(motivo) +
" | tratado como falha local"
);
} }
static void RegistrarReadyDropADS(uint32_t inicioMs) { static void RegistrarReadyDropADS(uint32_t inicioMs) {
@ -2469,6 +2967,7 @@ class I2CService {
portEXIT_CRITICAL(&_estadoLock); portEXIT_CRITICAL(&_estadoLock);
// READY pertence a infraestrutura global do barramento.
RegistrarFalhaTransacao("READY caiu durante leitura ADS"); RegistrarFalhaTransacao("READY caiu durante leitura ADS");
} }
@ -2481,10 +2980,14 @@ class I2CService {
AtualizarMaxDuracaoADS(inicioMs); AtualizarMaxDuracaoADS(inicioMs);
IncrementarFalhaIoADS(); IncrementarFalhaIoADS();
RegistrarFalhaTransacao( // Erro de I/O do ADS continua local. O chamador decide quando
String("ADS ") + etapa + // tentar power-cycle do ADS, sem alterar a geracao.
MostrarLog(
"ADS",
String(etapa) +
" | erro=" + String(erro) + " | erro=" + String(erro) +
" | duracao=" + String(duracaoMs) + "ms" " | duracao=" +
String(duracaoMs) + "ms"
); );
} }
@ -2518,7 +3021,10 @@ class I2CService {
endereco == 0xFF || endereco == 0xFF ||
IndiceADS(endereco) < 0 IndiceADS(endereco) < 0
) { ) {
RegistrarFalhaTransacao("ADS sem endereco ativo"); MostrarLog(
"ADS",
"Sem endereco ativo para leitura"
);
return false; return false;
} }
@ -2769,4 +3275,16 @@ uint32_t I2CService::_timeoutsAds = 0;
uint32_t I2CService::_readyDropsAds = 0; uint32_t I2CService::_readyDropsAds = 0;
uint32_t I2CService::_falhasIoAds = 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 #endif

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,7 @@ bool Conectado = false;
bool Conectar = false; bool Conectar = false;
int _TaxaAmostragem = 1000; int _TaxaAmostragem = 1000;
bool RequisitarDados = false; bool RequisitarDados = true;
int latenciaLoop = 0; int latenciaLoop = 0;
int qtdEnviosStatus = 0; int qtdEnviosStatus = 0;
int maxEnviosStatus = 5; int maxEnviosStatus = 5;
@ -76,9 +76,9 @@ uint32_t publicacaoMaxMs = 0;
// Aquisição física é responsabilidade exclusiva da TelemetriaTask. // Aquisição física é responsabilidade exclusiva da TelemetriaTask.
// O caminho CAN apenas serializa os snapshots já disponíveis em memória. // 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_CORRENTE_MS = 500; // 2 Hz
static constexpr uint32_t PERIODO_NTC_MS = 500; // 2 Hz static constexpr uint32_t PERIODO_NTC_MS = 1000; // 1 Hz
static constexpr uint32_t PERIODO_RETRY_I2C_MS = 10000; // retry lento 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_RETRY_SENSOR_MS = 10000;
static constexpr uint32_t PERIODO_DIAG_TELEMETRIA_MS = 10000; static constexpr uint32_t PERIODO_DIAG_TELEMETRIA_MS = 10000;
@ -139,10 +139,14 @@ Rele_v2* ObterRelePorId(uint8_t idNum) {
#pragma endregion #pragma endregion
#pragma region TEMPERATURA NTC #pragma region TEMPERATURA NTC
SensorTemperaturaNtc sNTC_MotorET("TMVET", 1, 6, TiposBarramentos::CONTROLADOR, -1, 0.0f, false); //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_MotorET("TMVET", 1, 0, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true);
SensorTemperaturaNtc sNTC_MotorEF("TMVEF", 2, 8, TiposBarramentos::CONTROLADOR, -1, 0.0f, false); //SensorTemperaturaNtc sNTC_MotorDT("TMVDT", 3, 7, TiposBarramentos::CONTROLADOR, -1, 0.0f, true);
SensorTemperaturaNtc sNTC_MotorDF("TMVDF", 4, 9, 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) { SensorTemperaturaNtc* ObterTempNtcPorId(uint8_t idNum) {
switch (idNum) { switch (idNum) {
@ -195,6 +199,156 @@ SensorMagnetico_v2* ObterSensorMagneticoPorId(uint8_t idNum) {
} }
#pragma endregion #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) { void MostrarLog(String mensagem) {
bool _debugMode = false; bool _debugMode = false;
if (_debugMode) { if (_debugMode) {
@ -203,9 +357,9 @@ void MostrarLog(String mensagem) {
} }
void inicializarDependenciasI2C() { void inicializarDependenciasI2C() {
I2CService::DefinirPinos(1, 2, -1, -1, 39, 40); I2CService::DefinirPinos(1, 2, 10, 11, 39, 40);
I2CService::IniciarI2C(); I2CService::IniciarI2C();
//I2CService::IniciarADS(); I2CService::IniciarADS();
I2CService::IniciarMUX(); I2CService::IniciarMUX();
//I2CService::IniciarMCP(); //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) { void TelemetriaTask(void* pv) {
TickType_t ultimaCorrente = 0; TickType_t ultimaCorrente = 0;
TickType_t ultimoNtc = 0; TickType_t ultimoNtc = 0;
@ -271,18 +449,18 @@ void TelemetriaTask(void* pv) {
const TickType_t agora = xTaskGetTickCount(); const TickType_t agora = xTaskGetTickCount();
if ((agora - ultimaCorrente) >= pdMS_TO_TICKS(PERIODO_CORRENTE_MS)) { if ((agora - ultimaCorrente) >= pdMS_TO_TICKS(PERIODO_CORRENTE_MS)) {
ultimaCorrente = agora;
const uint32_t inicioAquisicao = millis(); const uint32_t inicioAquisicao = millis();
AtualizarSensoresCorrente(); AtualizarSensoresCorrente();
ultimaCorrente = xTaskGetTickCount();
const uint32_t duracao = millis() - inicioAquisicao; const uint32_t duracao = millis() - inicioAquisicao;
if (duracao > maxAquisicaoCorrenteMs) maxAquisicaoCorrenteMs = duracao; if (duracao > maxAquisicaoCorrenteMs) maxAquisicaoCorrenteMs = duracao;
ciclosTelemetria++; ciclosTelemetria++;
} }
if ((agora - ultimoNtc) >= pdMS_TO_TICKS(PERIODO_NTC_MS)) { if ((agora - ultimoNtc) >= pdMS_TO_TICKS(PERIODO_NTC_MS)) {
ultimoNtc = agora;
const uint32_t inicioAquisicao = millis(); const uint32_t inicioAquisicao = millis();
AtualizarSensoresNtc(); AtualizarSensoresNtc();
ultimoNtc = xTaskGetTickCount();
const uint32_t duracao = millis() - inicioAquisicao; const uint32_t duracao = millis() - inicioAquisicao;
if (duracao > maxAquisicaoNtcMs) maxAquisicaoNtcMs = duracao; if (duracao > maxAquisicaoNtcMs) maxAquisicaoNtcMs = duracao;
} }
@ -295,6 +473,7 @@ void TelemetriaTask(void* pv) {
if ((agora - ultimoRetrySensor) >= pdMS_TO_TICKS(PERIODO_RETRY_SENSOR_MS)) { if ((agora - ultimoRetrySensor) >= pdMS_TO_TICKS(PERIODO_RETRY_SENSOR_MS)) {
ultimoRetrySensor = agora; ultimoRetrySensor = agora;
TentarReinicializarSensoresCorrente(); TentarReinicializarSensoresCorrente();
TentarReinicializarSensoresNtc();
} }
if ((agora - ultimoDiag) >= pdMS_TO_TICKS(PERIODO_DIAG_TELEMETRIA_MS)) { if ((agora - ultimoDiag) >= pdMS_TO_TICKS(PERIODO_DIAG_TELEMETRIA_MS)) {
@ -342,21 +521,33 @@ void setup() {
sLED_Manual.Inicializar(); sLED_Manual.Inicializar();
sLED_Automatico.Inicializar(); sLED_Automatico.Inicializar();
sLED_Operacao.Inicializar(); sLED_Operacao.Inicializar();
sNTC_MotorET.Inicializar();
sNTC_MotorDT.Inicializar(); if (I2CService::I2CIniciado) {
sNTC_MotorEF.Inicializar();
sNTC_MotorDF.Inicializar(); if (I2CService::AdsIniciado) {
sCOR_3V0.Inicializar(); sNTC_MotorET.Inicializar();
sCOR_5V0.Inicializar(); sNTC_MotorDT.Inicializar();
sCOR_7V2.Inicializar(); sNTC_MotorEF.Inicializar();
sCOR_12V.Inicializar(); sNTC_MotorDF.Inicializar();
sCOR_19V.Inicializar(); }
sCOR_24V.Inicializar();
sCOR_36V.Inicializar(); if (I2CService::MuxIniciado) {
//sMAG_MotorET.Inicializar(); sCOR_3V0.Inicializar();
//sMAG_MotorDT.Inicializar(); sCOR_5V0.Inicializar();
//sMAG_MotorEF.Inicializar(); sCOR_7V2.Inicializar();
//sMAG_MotorDF.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_Manual);
ledManager.Adicionar(&sLED_Automatico); ledManager.Adicionar(&sLED_Automatico);
@ -439,9 +630,33 @@ void enviarDadosSensores(uint8_t id_num, CanMessagePosicaoDados posicao) {
bool todosIDs = id_num == canService.ID_Num_sTOD; bool todosIDs = id_num == canService.ID_Num_sTOD;
if (id_num == canService.ID_Num_sMOD) { if (id_num == canService.ID_Num_sMOD) {
EnviarDadosCAN(canService.MontarFrameReqStatusMod(D_Code, cfg.conectado, VERSION)); switch (posicao) {
if (PublicacaoMutex != nullptr) xSemaphoreGive(PublicacaoMutex); case CanMessagePosicaoDados::Status:
return; 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) if (todosIDs)