diff --git a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs index 66e99f9cb..91352cb9b 100644 --- a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs +++ b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs @@ -1,10 +1,11 @@ -using AgroBase.Services; +using AgroBase.Services; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using static AgroBase.Models.Enums; @@ -15,8 +16,8 @@ namespace AgroBase.Models.Modules { public T_Code Dispositivo { get; set; } = T_Code.Atu; public string Modulo_ID { get; set; } = T_Code.Atu.ToString(); - public byte _EnderecoCAN_Tx { get; set; } = 0x13; - public byte _EnderecoCAN_Rx { get; set; } = 0x14; + public byte _EnderecoCAN_Tx { get; set; } = 0x12; + public byte _EnderecoCAN_Rx { get; set; } = 0x12; public int _TaxaAmostragem { get; set; } = 1000; public double FrequenciaBase { @@ -27,22 +28,43 @@ namespace AgroBase.Models.Modules } public bool RequisitarDados { get; set; } = true; public static int TempoLimiteConexao { get; set; } = 8000; + private const int IntervaloDescobertaDesconectadoMs = 2000; + private DateTime _ultimaDescobertaEnviada = DateTime.MinValue; + private readonly object _estadoLock = new object(); + private readonly SemaphoreSlim _calibragemGate = new SemaphoreSlim(1, 1); + private readonly object _calibragemCtsLock = new object(); + private CancellationTokenSource _calibragemCts; + private bool _sincronizacaoSeguraPendente; + + [JsonIgnore] + internal object EstadoSyncRoot => _estadoLock; public bool Conectado { get { - DateTime Agora = DateTime.Now; - bool modConectado = DadosLeitura.UltimoComandoRespondido.AddMilliseconds(TempoLimiteConexao) > Agora; - return modConectado; + lock (_estadoLock) + { + DateTime agora = DateTime.Now; + return DadosLeitura != null && + DadosLeitura.UltimoComandoRespondido != DateTime.MinValue && + DadosLeitura.UltimoComandoRespondido.AddMilliseconds(TempoLimiteConexao) > agora; + } } } - public bool ConexaoAtiva + + [JsonIgnore] + public bool TelemetriaRecente => Conectado; + + [JsonIgnore] + public bool FirmwareDeclarouConectado { get { - return Variaveis.OperacaoEmAndamento.Sensoriamento?.OperadorSaude?.ModulosSaude?.Any(x => x.modulo == Dispositivo && x.status == StatusModulo.Operante) ?? false; + lock (_estadoLock) + return DadosLeitura?.Conectado == true; } } + public bool ConexaoAtiva => TelemetriaRecente; public bool AtuPronto { get; set; } = false; public double AnguloBarraEsquerda { get; set; } @@ -64,6 +86,7 @@ namespace AgroBase.Models.Modules public AsyncTaskTimerModel tmrRegistrador; private int _TempoReconexaoComponentes = 5; private int _VezesLeituraReconexao = 0; + public AtuadorMensagemJsonModel DadosLeitura { get; set; } = new AtuadorMensagemJsonModel(); [JsonIgnore] @@ -121,7 +144,7 @@ namespace AgroBase.Models.Modules #region VAZAO [JsonIgnore] - private SensorModel SensorFluxoLinha => Sensores.FirstOrDefault(x => x.Componente == S_Code.sFLX && x.ID == "FLXLN"); + private SensorModel SensorFluxoLinha => Sensores?.FirstOrDefault(x => x.Componente == S_Code.sFLX && x.ID == "FLXLN"); const double FatorCalibracaoFluxo = 0.85; @@ -184,7 +207,7 @@ namespace AgroBase.Models.Modules { get { - return Sensores.FirstOrDefault(x => x.Componente == S_Code.sPRS && x.ID == "PRSLN"); + return Sensores?.FirstOrDefault(x => x.Componente == S_Code.sPRS && x.ID == "PRSLN"); } } public double _PressaoVmin { get; set; } = 0.0; @@ -1279,13 +1302,23 @@ namespace AgroBase.Models.Modules public void LimparDados(T _dados) { var Dados = _dados as AtuadorModel; - Dados.DadosLeitura = new AtuadorMensagemJsonModel(); - Dados.DadosLeitura.Momento = DateTime.MinValue; + + if (Dados == null) + return; + + /* + * Iniciar uma nova operação não significa que o hardware se + * desconectou. Preserva o envelope de comunicação e as últimas + * leituras físicas; zera apenas comandos, métricas e históricos. + * Isso evita um snapshot falso de ATU/componentes desconectados + * entre LimparDados() e a próxima rodada de telemetria CAN. + */ Dados.AtuPronto = false; Dados.PercentualErvasTerreno = 0; - Dados.BombasPressurizadoras.ForEach(x => x.Funcoes = x.Funcoes.Distinct().ToList()); + Dados.BombasPressurizadoras?.ForEach(x => + x.Funcoes = (x.Funcoes ?? new List()).Distinct().ToList()); - ReiniciarLeituras(Dados, true); + ReiniciarLeituras(Dados, false); PrepararAtuadorParaOperacao(Dados, resetarComandos: true); } @@ -1319,6 +1352,8 @@ namespace AgroBase.Models.Modules x.UltimoStatusDesejado = false; x.UltimoAnguloDesejado = x.AnguloNeutro; x.UltimoComandoEnviado = DateTime.MinValue; + x.UltimoComandoEnfileirado = DateTime.MinValue; + x.UltimaRespostaPosteriorAoComando = DateTime.MinValue; x.ReiniciarHistoricoAtuacao(true); @@ -1346,6 +1381,8 @@ namespace AgroBase.Models.Modules x.UltimaPotenciaDesejada = 0; x.UltimaPressaoDesejada = 0; x.UltimoComandoEnviado = DateTime.MinValue; + x.UltimoComandoEnfileirado = DateTime.MinValue; + x.UltimaRespostaPosteriorAoComando = DateTime.MinValue; if (apagarValoresLeituras) { @@ -1494,9 +1531,7 @@ namespace AgroBase.Models.Modules double pressaoEfetiva = bomba.Sensor_ID_Num > -1 ? (statusDesejado ? bomba.PressaoSP : 0) : bomba.PressaoSP; - Estado estadoDesejado = statusDesejado ? Estado.Ligado : Estado.Desligado; - - bool estadoDivergente = estadoDesejado != bomba._EstadoLeitura; + bool estadoDivergente = EstadoFisicoBombaDivergente(bomba, statusDesejado, true); bool potenciaDivergente = Math.Abs(bomba._PotenciaSP - potenciaEfetiva) > 1.0; @@ -1534,7 +1569,6 @@ namespace AgroBase.Models.Modules if (Conectado) { DefinirDispositivo(); - op.DispAtu?.EnviarProtocolosConfiguracao(); } return Conectado; @@ -1544,9 +1578,17 @@ namespace AgroBase.Models.Modules { var op = Variaveis.OperacaoEmAndamento; - if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo && x.Mod_ID == Modulo_ID)) + if (op?.Sensoriamento?.Operacao == null) + return; + + bool novoMapeamento = false; + + lock (SerialService.DispositivosMapeados) { - SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel() + if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo && x.Mod_ID == Modulo_ID)) + return; + + SerialService.AdicionarOuAtualizarDispositivoMapeado(new DispositivoDetalhesModel() { Dispositivo = DadosLeitura.Dispositivo, Endereco = CanManager.CanService._portName, @@ -1554,19 +1596,46 @@ namespace AgroBase.Models.Modules Mod_ID = Modulo_ID }); - op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo] = new Dictionary(); - op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Conectado, 0, "Dispositivo conectado"); + novoMapeamento = true; } + + /* Redescoberta não pode apagar a calibragem já obtida. */ + if (op.Sensoriamento.Operacao.ModsCalibragem == null) + op.Sensoriamento.Operacao.ModsCalibragem = new Dictionary>(); + + if (!op.Sensoriamento.Operacao.ModsCalibragem.ContainsKey(Dispositivo)) + op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo] = new Dictionary(); + + if (novoMapeamento) + op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Conectado, 0, "Dispositivo conectado"); + } + + internal void ConfirmarMapeamentoAposDescoberta() + { + if (DadosLeitura?.Conectado == true) + DefinirDispositivo(); } private void RequisitarDadosDescoberta() { - Variaveis.OperacaoEmAndamento.DispAtu?.AdicionarMensagemFila(Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Status, CanMessagePosicaoDados.Status, null, true, false, true); + _ultimaDescobertaEnviada = DateTime.Now; + Variaveis.OperacaoEmAndamento?.DispAtu?.AdicionarMensagemFila(Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Status, CanMessagePosicaoDados.Status, null, true, false, true); + } + + private void ManterDescobertaAtiva(DateTime agora) + { + if (Conectado) + return; + + if ((agora - _ultimaDescobertaEnviada).TotalMilliseconds < IntervaloDescobertaDesconectadoMs) + return; + + RequisitarDadosDescoberta(); } public void RequisitarDadosModulo(int ID_Num, CanMessagePosicaoDados posicaoTx, CanMessagePosicaoDados posicaoRx, bool aguardaResposta) { - Variaveis.OperacaoEmAndamento.DispAtu?.AdicionarMensagemFila(ID_Num, posicaoTx, posicaoRx, null, aguardaResposta); + Variaveis.OperacaoEmAndamento?.DispAtu?.AdicionarMensagemFila(ID_Num, posicaoTx, posicaoRx, null, aguardaResposta); } public (int, CanMessagePosicaoDados, byte[]) ProtocoloComandoReset() @@ -1633,11 +1702,8 @@ namespace AgroBase.Models.Modules foreach (var sensor in Sensores.Where(x => (x.Aferir && (ConfigurarTodos ? true : x.Inicializado != Conectar)) || (!x.Aferir && x.Inicializado))) { - if (!sensor.Aferir && sensor.Inicializado) - { - Conectar = false; - } - (CanMessagePosicaoDados posicao, byte[] payload) = sensor.ProtocoloConfiguracao(Pinout, Conectar, Dispositivo); + bool conectarComponente = Conectar && sensor.Aferir; + (CanMessagePosicaoDados posicao, byte[] payload) = sensor.ProtocoloConfiguracao(Pinout, conectarComponente, Dispositivo); if (payload != null) { config.Add((sensor.ID_Num, posicao, payload)); @@ -1653,11 +1719,8 @@ namespace AgroBase.Models.Modules foreach (var bico in BicosPulverizadores.Where(x => (x.Comandar && (ConfigurarTodos ? true : x.Inicializado != Conectar)) || (!x.Comandar && x.Inicializado))) { - if (!bico.Comandar && bico.Inicializado) - { - Conectar = false; - } - (CanMessagePosicaoDados posicao, var payload) = bico.ProtocoloConfiguracao(Pinout, Conectar); + bool conectarComponente = Conectar && bico.Comandar; + (CanMessagePosicaoDados posicao, var payload) = bico.ProtocoloConfiguracao(Pinout, conectarComponente); if (payload != null) { config.Add((bico.ID_Num, posicao, payload)); @@ -1673,11 +1736,8 @@ namespace AgroBase.Models.Modules foreach (var bomba in BombasPressurizadoras.Where(x => (x.Comandar && (ConfigurarTodos ? true : x.Inicializado != Conectar)) || (!x.Comandar && x.Inicializado))) { - if (!bomba.Comandar && bomba.Inicializado) - { - Conectar = false; - } - List<(CanMessagePosicaoDados, byte[])> dados = bomba.ProtocoloConfiguracao(Pinout, Conectar); + bool conectarComponente = Conectar && bomba.Comandar; + List<(CanMessagePosicaoDados, byte[])> dados = bomba.ProtocoloConfiguracao(Pinout, conectarComponente); foreach (var dado in dados) { config.Add((bomba.ID_Num, dado.Item1, dado.Item2)); @@ -1717,7 +1777,7 @@ namespace AgroBase.Models.Modules if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo) && (_VezesLeituraReconexao >= _TempoReconexaoComponentes)) { op.DispAtu?.EnviarProtocolosConfiguracao(); - + _VezesLeituraReconexao = 0; } else @@ -1725,7 +1785,17 @@ namespace AgroBase.Models.Modules _VezesLeituraReconexao++; } - if (DadosLeitura.Conectado) + if (BloqueioAbsolutoAtuacao(out string motivoBloqueio)) + CancelarCalibragem(motivoBloqueio); + + /* + * Se a telemetria envelheceu, continua pingando a descoberta. + * O ESP32 pode estar vivo e configurado; nesse caso basta o C# + * voltar a reconhecer respostas válidas, sem resetar o módulo. + */ + ManterDescobertaAtiva(DateTime.Now); + + if (Conectado) { if (RequisitarDados) { @@ -1808,7 +1878,7 @@ namespace AgroBase.Models.Modules * Verifica o ângulo também quando desligado, * para garantir o retorno à posição neutra. */ - bool anguloDivergente = Math.Abs(anguloDesejado - bico._AnguloBicoLeitura) > 1.0; + bool anguloDivergente = bico.ComMovimento && Math.Abs(anguloDesejado - bico._AnguloBicoLeitura) > 1.0; if (estadoDivergente || anguloDivergente) { @@ -1898,7 +1968,7 @@ namespace AgroBase.Models.Modules pressaoEfetiva = bomba.PressaoSP; } - bool estadoDivergente = estadoDesejado != bomba._EstadoLeitura; + bool estadoDivergente = EstadoFisicoBombaDivergente(bomba, statusDesejado, ehBombaPrincipal); bool potenciaDivergente = Math.Abs(bomba._PotenciaSP - potenciaEfetiva) > 1.0; @@ -1918,6 +1988,37 @@ namespace AgroBase.Models.Modules } } + private bool EstadoFisicoBombaDivergente(AtuadorBombaModel bomba, bool statusDesejado, bool ehBombaPrincipal) + { + bool possuiBicoFisicamenteAberto = + BicosPulverizadores.Any(x => + x != null && + x.Inicializado && + x._EstadoLeitura == Estado.Ligado); + + /* + * Agitador não depende de bicos. + * + * A bomba principal somente deve ficar fisicamente desligada + * enquanto habilitada quando o intertravamento estiver ativo + * e nenhum bico estiver realmente aberto. + */ + bool deveEstarFisicamenteLigada = + statusDesejado && + ( + !ehBombaPrincipal || + !bomba.Controle.DesligarSemBicosAbertos || + possuiBicoFisicamenteAberto + ); + + Estado estadoFisicoEsperado = + deveEstarFisicamenteLigada + ? Estado.Ligado + : Estado.Desligado; + + return estadoFisicoEsperado != bomba._EstadoLeitura; + } + public void EnviarComandoFisicoReset() { var dispAtu = Variaveis.OperacaoEmAndamento?.DispAtu; @@ -1929,6 +2030,79 @@ namespace AgroBase.Models.Modules dispAtu.AdicionarMensagemFila(id_num, dado, dado, data, false); } + private bool BloqueioAbsolutoAtuacao(out string motivo) + { + motivo = null; + + if (Variaveis.Fechando) + { + motivo = "aplicação encerrando"; + return true; + } + + var op = Variaveis.OperacaoEmAndamento; + var operacao = op?.Sensoriamento?.Operacao; + + if (operacao == null) + { + motivo = "operação indisponível"; + return true; + } + + if (operacao.Emergencia) + { + motivo = "emergência ativa"; + return true; + } + + if (operacao.Finalizando) + { + motivo = "operação finalizando"; + return true; + } + + if (operacao.StatusOperacaoAtual == StatusOperacao.Concluido) + { + motivo = "operação concluída"; + return true; + } + + return false; + } + + private bool PodeEnviarComandoFisico(bool statusLigado, string componente) + { + var dados = Variaveis.OperacaoEmAndamento?.DispAtu?.Dados; + + if (dados == null || !dados.Conectado) + return false; + + /* Desligar continua permitido durante emergência/finalização. */ + if (!statusLigado) + return true; + + if (!BloqueioAbsolutoAtuacao(out string motivo)) + return true; + + Variaveis.MostrarLog($"[ATU/SEGURANCA] Comando ON bloqueado para {componente}: {motivo}"); + return false; + } + + private bool PodeReenviarComando(DateTime ultimoEnfileiramento, DateTime ultimaRespostaPosterior, DateTime agora) + { + if (ultimoEnfileiramento == DateTime.MinValue) + return true; + + if (ultimaRespostaPosterior >= ultimoEnfileiramento) + return true; + + /* + * Ainda não chegou nenhuma telemetria posterior. + * Aguarda pelo menos uma janela antes de reenfileirar. + */ + return (agora - ultimoEnfileiramento).TotalMilliseconds >= 1500; + } + public void EnviarComandoFisicoBico( AtuadorBicoModel bico, CanMessagePosicaoDados dado, @@ -1939,7 +2113,7 @@ namespace AgroBase.Models.Modules { var dispAtu = Variaveis.OperacaoEmAndamento?.DispAtu; - if (dispAtu?.Dados?.Conectado != true || bico == null) + if (bico == null || !PodeEnviarComandoFisico(status, bico.ID)) return; bool comandoControle = bico.ComandoAtuar; @@ -1962,7 +2136,7 @@ namespace AgroBase.Models.Modules false ); - bico.UltimoComandoEnviado = DateTime.Now; + bico.UltimoComandoEnfileirado = DateTime.Now; bico.UltimoStatusDesejado = status; if (angulo.HasValue) @@ -1994,8 +2168,9 @@ namespace AgroBase.Models.Modules ) { var dispAtu = Variaveis.OperacaoEmAndamento?.DispAtu; + bool ehComandoEstado = dado == CanMessagePosicaoDados.Command1; - if (dispAtu?.Dados?.Conectado != true || bomba == null) + if (bomba == null || !PodeEnviarComandoFisico(ehComandoEstado && status, bomba.ID)) return; bool comandoControle = bomba.ComandoAtuar; @@ -2008,45 +2183,48 @@ namespace AgroBase.Models.Modules double kiControle = bomba.Controle.Ki; double kdControle = bomba.Controle.Kd; - bool ehComandoEstado = dado == CanMessagePosicaoDados.Command1; - - if (ehComandoEstado) + try { - bomba.ComandoAtuar = status; - if (potencia.HasValue) bomba.PotenciaSP = potencia.Value; - if (pressao.HasValue) bomba.PressaoSP = pressao.Value; + if (ehComandoEstado) + { + bomba.ComandoAtuar = status; + if (potencia.HasValue) bomba.PotenciaSP = potencia.Value; + if (pressao.HasValue) bomba.PressaoSP = pressao.Value; + } + + if (modo.HasValue) bomba.Controle.Modo = modo.Value; + if (desligarSemBicoAberto.HasValue) bomba.Controle.DesligarSemBicosAbertos = desligarSemBicoAberto.Value; + if (potenciaMinPid.HasValue) bomba.Controle.PotenciaMinPID = potenciaMinPid.Value; + if (Kp.HasValue) bomba.Controle.Kp = Kp.Value; + if (Ki.HasValue) bomba.Controle.Ki = Ki.Value; + if (Kd.HasValue) bomba.Controle.Kd = Kd.Value; + + (_, _, byte[] data) = bomba.ProtocoloComando(dado); + dispAtu.AdicionarMensagemFila(bomba.ID_Num, dado, dado, data, false); + + if (ehComandoEstado) + { + DateTime enfileiradoEm = DateTime.Now; + bomba.UltimoComandoEnfileirado = enfileiradoEm; + bomba.UltimoStatusDesejado = status; + if (potencia.HasValue) bomba.UltimaPotenciaDesejada = potencia.Value; + if (pressao.HasValue) bomba.UltimaPressaoDesejada = pressao.Value; + } } - - if (modo.HasValue) bomba.Controle.Modo = modo.Value; - if (desligarSemBicoAberto.HasValue) bomba.Controle.DesligarSemBicosAbertos = desligarSemBicoAberto.Value; - if (potenciaMinPid.HasValue) bomba.Controle.PotenciaMinPID = potenciaMinPid.Value; - if (Kp.HasValue) bomba.Controle.Kp = Kp.Value; - if (Ki.HasValue) bomba.Controle.Ki = Ki.Value; - if (Kd.HasValue) bomba.Controle.Kd = Kd.Value; - - (_, _, byte[] data) = bomba.ProtocoloComando(dado); - - dispAtu.AdicionarMensagemFila(bomba.ID_Num, dado, dado, data, false); - - if (ehComandoEstado) + finally { - bomba.UltimoComandoEnviado = DateTime.Now; - bomba.UltimoStatusDesejado = status; - if (potencia.HasValue) bomba.UltimaPotenciaDesejada = potencia.Value; - if (pressao.HasValue) bomba.UltimaPressaoDesejada = pressao.Value; - } - - if (!setarControle) - { - bomba.ComandoAtuar = comandoControle; - bomba.PotenciaSP = potenciaControle; - bomba.PressaoSP = pressaoControle; - bomba.Controle.Modo = modoControle; - bomba.Controle.DesligarSemBicosAbertos = desligarSemBicoAbertoControle; - bomba.Controle.PotenciaMinPID = potenciaMinPidControle; - bomba.Controle.Kp = kpControle; - bomba.Controle.Ki = kiControle; - bomba.Controle.Kd = kdControle; + if (!setarControle) + { + bomba.ComandoAtuar = comandoControle; + bomba.PotenciaSP = potenciaControle; + bomba.PressaoSP = pressaoControle; + bomba.Controle.Modo = modoControle; + bomba.Controle.DesligarSemBicosAbertos = desligarSemBicoAbertoControle; + bomba.Controle.PotenciaMinPID = potenciaMinPidControle; + bomba.Controle.Kp = kpControle; + bomba.Controle.Ki = kiControle; + bomba.Controle.Kd = kdControle; + } } } @@ -2054,32 +2232,128 @@ namespace AgroBase.Models.Modules private void AtualizaComponentesIniciados() { - DateTime Agora = DateTime.Now; - bool modConectado = DadosLeitura.UltimoComandoRespondido.AddMilliseconds(TempoLimiteConexao) > Agora; - if (!modConectado && DadosLeitura.Conectado) + lock (_estadoLock) { - DadosLeitura.Conectado = false; - } - foreach (var bomba in BombasPressurizadoras.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) - { - bomba.Inicializado = false; - } - foreach (var bico in BicosPulverizadores.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) - { - bico.Inicializado = false; - bico.AtualizarEstadoTrecho(false); - } - foreach (var sensor in Sensores.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) - { - sensor.Inicializado = false; + DateTime Agora = DateTime.Now; + bool modConectado = DadosLeitura.UltimoComandoRespondido != DateTime.MinValue && + DadosLeitura.UltimoComandoRespondido.AddMilliseconds(TempoLimiteConexao) > Agora; + if (!modConectado && DadosLeitura.Conectado) + { + DadosLeitura.Conectado = false; + AtuPronto = false; + _sincronizacaoSeguraPendente = true; + } + foreach (var bomba in BombasPressurizadoras.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) + { + bomba.Inicializado = false; + } + foreach (var bico in BicosPulverizadores.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) + { + bico.Inicializado = false; + bico.AtualizarEstadoTrecho(false); + } + foreach (var sensor in Sensores.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) + { + sensor.Inicializado = false; + } } } + private void ValidarCalibragemAindaPermitida(CancellationToken token) + { + token.ThrowIfCancellationRequested(); - private async Task RealizarTestesIniciais(bool forcar) + if (BloqueioAbsolutoAtuacao(out string motivo)) + throw new OperationCanceledException($"Calibragem cancelada: {motivo}", token); + } + + private async Task AguardarCalibragemAsync(int milissegundos, CancellationToken token) + { + await Task.Delay(milissegundos, token); + ValidarCalibragemAindaPermitida(token); + } + + private async Task AguardarCondicaoCalibragemAsync(Func condicao, int timeoutMs, int intervaloMs, CancellationToken token) + { + DateTime limite = DateTime.Now.AddMilliseconds(timeoutMs); + + while (DateTime.Now < limite) + { + ValidarCalibragemAindaPermitida(token); + + if (condicao()) + return true; + + await Task.Delay(intervaloMs, token); + } + + ValidarCalibragemAindaPermitida(token); + return condicao(); + } + + public void CancelarCalibragem(string motivo = null) + { + lock (_calibragemCtsLock) + { + var cts = _calibragemCts; + + if (cts == null || cts.IsCancellationRequested) + return; + + Variaveis.MostrarLog($"[ATU/CALIBRAGEM] Cancelamento solicitado{(string.IsNullOrWhiteSpace(motivo) ? "" : ": " + motivo)}"); + cts.Cancel(); + } + } + + private void EnviarEstadoSeguroCalibragem() + { + foreach (var bico in BicosPulverizadores ?? new List()) + { + if (bico?.Inicializado == true) + EnviarComandoFisicoBico(bico, CanMessagePosicaoDados.Command1, setarControle: true, status: false, angulo: bico.AnguloNeutro); + } + + foreach (var bomba in BombasPressurizadoras ?? new List()) + { + if (bomba?.Inicializado == true) + { + double pressaoOff = bomba.Sensor_ID_Num > -1 ? 0 : -1; + EnviarComandoFisicoBomba(bomba, CanMessagePosicaoDados.Command1, setarControle: true, status: false, potencia: 0, pressao: pressaoOff); + } + } + } + + internal void SincronizarEstadoSeguroAposReconexao() + { + if (!_sincronizacaoSeguraPendente || !Conectado) + return; + + Variaveis.MostrarLog("[ATU/RECONEXAO] Resposta válida recebida; sincronizando saídas em estado seguro"); + + foreach (var bico in BicosPulverizadores ?? new List()) + { + if (bico?.Comandar == true) + EnviarComandoFisicoBico(bico, CanMessagePosicaoDados.Command1, setarControle: false, status: false, angulo: bico.AnguloNeutro); + } + + foreach (var bomba in BombasPressurizadoras ?? new List()) + { + if (bomba?.Comandar == true) + { + double pressaoOff = bomba.Sensor_ID_Num > -1 ? 0 : -1; + EnviarComandoFisicoBomba(bomba, CanMessagePosicaoDados.Command1, setarControle: false, status: false, potencia: 0, pressao: pressaoOff); + } + } + + _sincronizacaoSeguraPendente = false; + } + + private async Task RealizarTestesIniciais(bool forcar, CancellationToken token) { var op = Variaveis.OperacaoEmAndamento; + ValidarCalibragemAindaPermitida(token); + if (!op.Sensoriamento.Operacao.ModsCalibragem.ContainsKey(Dispositivo)) op.Sensoriamento.Operacao.ModsCalibragem.Add(Dispositivo, new Dictionary()); var calAtu = op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo]; @@ -2087,6 +2361,8 @@ namespace AgroBase.Models.Modules // Ligar bomba foreach (var Bomba in BombasPressurizadoras.Where(x => x.Comandar)) { + ValidarCalibragemAindaPermitida(token); + if (!forcar && op.ModuloEstaCalibrado(Dispositivo, Bomba.ID)) continue; @@ -2104,7 +2380,7 @@ namespace AgroBase.Models.Modules continue; } - bool EhBombaPressurizadora = Bomba.ID == BombaPressurizadora.ID; + bool EhBombaPressurizadora = Bomba.ID == BombaPressurizadora?.ID; if (EhBombaPressurizadora) { @@ -2128,13 +2404,13 @@ namespace AgroBase.Models.Modules pressao: 0 ); - await Task.Delay(500); + await AguardarCalibragemAsync(500, token); // Alivia pressao da linha BicosPulverizadores.Where(x => x.Inicializado && x.Comandar).ToList().ForEach(Bico => EnviarComandoFisicoBico(Bico, CanMessagePosicaoDados.Command1, setarControle: true, status: true)); - await Task.Delay(3000); + await AguardarCalibragemAsync(3000, token); BicosPulverizadores.Where(x => x.Inicializado && x.Comandar).ToList().ForEach(Bico => EnviarComandoFisicoBico(Bico, CanMessagePosicaoDados.Command1, setarControle: true, status: false)); - await Task.Delay(1000); + await AguardarCalibragemAsync(1000, token); double pressaoTeste = 50.0; double difPressaoTeste = 20.0; @@ -2160,7 +2436,7 @@ namespace AgroBase.Models.Modules pressao: pressaoTeste ); - await FuncoesGlobais.AguardarCondicaoAsync(() => Bomba.AtingiuPressao(pressaoTeste), 3000, 200); + await AguardarCondicaoCalibragemAsync(() => Bomba.AtingiuPressao(pressaoTeste), 3000, 200, token); bool pressaoSubiu = Bomba._PressaoAtual - pressaoInicialTeste >= difPressaoTeste; BombaOk = (Bomba.AtingiuPressao(pressaoTeste) && pressaoSubiu) || !Bomba.Mandatorio; @@ -2201,35 +2477,45 @@ namespace AgroBase.Models.Modules } else { - EnviarComandoFisicoBomba( - Bomba, - CanMessagePosicaoDados.Command1, - setarControle: true, - status: true, - potencia: 100, - pressao: -1 - ); + try + { + DateTime comandoEm = DateTime.Now; - bool BombaLigou() => Bomba.ComandoAtuar == (Bomba._EstadoLeitura == Estado.Ligado); - - await FuncoesGlobais.AguardarCondicaoAsync(BombaLigou, 3000, 200); - BombaOk = BombaLigou() || !Bomba.Mandatorio; - op.MarcarModuloCalibrado(Dispositivo, Bomba.ID, BombaOk); + EnviarComandoFisicoBomba( + Bomba, + CanMessagePosicaoDados.Command1, + setarControle: true, + status: true, + potencia: 100, + pressao: -1 + ); - EnviarComandoFisicoBomba( - Bomba, - CanMessagePosicaoDados.Command1, - setarControle: true, - status: false, - potencia: 100, - pressao: -1 - ); + bool BombaLigou() => + Bomba.UltimaLeitura > comandoEm && + Bomba._EstadoLeitura == Estado.Ligado; + + BombaOk = await AguardarCondicaoCalibragemAsync(BombaLigou, 3000, 100, token) || !Bomba.Mandatorio; + op.MarcarModuloCalibrado(Dispositivo, Bomba.ID, BombaOk); + } + finally + { + EnviarComandoFisicoBomba( + Bomba, + CanMessagePosicaoDados.Command1, + setarControle: true, + status: false, + potencia: 0, + pressao: -1 + ); + } } } // Testar os bicos foreach (var Bico in BicosPulverizadores.Where(x => x.Comandar)) { + ValidarCalibragemAindaPermitida(token); + calAtu.TryGetValue(Bico.ID, out var cal); if (!forcar && cal && calAtu[Bico.ID]) continue; @@ -2237,29 +2523,59 @@ namespace AgroBase.Models.Modules if (Bico.Inicializado) { - EnviarComandoFisicoBico( - Bico, - CanMessagePosicaoDados.Command1, - setarControle: true, - status: true - ); + bool abriu = false; + bool fechou = false; - await Task.Delay(200); + try + { + DateTime comandoAbrirEm = DateTime.Now; - EnviarComandoFisicoBico( - Bico, - CanMessagePosicaoDados.Command1, - setarControle: true, - status: false - ); + EnviarComandoFisicoBico( + Bico, + CanMessagePosicaoDados.Command1, + setarControle: true, + status: true, + angulo: Bico.AnguloAbertura + ); - await Task.Delay(200); + bool ConfirmouAberto() => + Bico.UltimaLeitura > comandoAbrirEm && + Bico._EstadoLeitura == Estado.Ligado; - op.MarcarModuloCalibrado(Dispositivo, Bico.ID, true); + abriu = await AguardarCondicaoCalibragemAsync(ConfirmouAberto, 1500, 75, token); + } + finally + { + DateTime comandoFecharEm = DateTime.Now; + + EnviarComandoFisicoBico( + Bico, + CanMessagePosicaoDados.Command1, + setarControle: true, + status: false, + angulo: Bico.AnguloNeutro + ); + + bool ConfirmouFechado() => + Bico.UltimaLeitura > comandoFecharEm && + Bico._EstadoLeitura == Estado.Desligado; + + try + { + fechou = await AguardarCondicaoCalibragemAsync(ConfirmouFechado, 1500, 75, token); + } + catch (OperationCanceledException) + { + /* O OFF já foi enfileirado; preserva o cancelamento. */ + throw; + } + } + + op.MarcarModuloCalibrado(Dispositivo, Bico.ID, abriu && fechou); } } - await Task.Delay(1000); + await AguardarCalibragemAsync(1000, token); var idsEsperados = BombasPressurizadoras .Where(x => x.Comandar) @@ -2283,41 +2599,71 @@ namespace AgroBase.Models.Modules if (op?.DispAtu?.Dados == null) return tasks; - tasks.Add(Task.Run(async () => - { - try - { - bool sucesso = await op.DispAtu.Dados.RealizarTestesIniciais(forcar); - - var calAtu = op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo]; - var v = calAtu.Select(x => $"{x.Key} " + (x.Value ? "Sim" : "Não")); - - if (sucesso) - { - Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Teste do módulo {Modulo_ID} finalizado com sucesso: " + string.Join(", ", v)); - - op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Operante, 100, $"Teste do módulo {Modulo_ID} finalizado com sucesso: " + string.Join(", ", v)); - } - else - { - Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v)); - - op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v)); - } - } - catch (Exception ex) - { - Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Erro no teste do módulo {Modulo_ID}: {ex.Message}"); - - op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Falha, 0, $"Erro no teste do módulo {Modulo_ID}: {ex.Message}"); - - throw; - } - })); + tasks.Add(ExecutarCalibragemProtegidaAsync(forcar)); return tasks; } + private async Task ExecutarCalibragemProtegidaAsync(bool forcar) + { + var op = Variaveis.OperacaoEmAndamento; + + await _calibragemGate.WaitAsync(); + + CancellationToken token; + lock (_calibragemCtsLock) + { + _calibragemCts?.Dispose(); + _calibragemCts = new CancellationTokenSource(); + token = _calibragemCts.Token; + } + + try + { + bool sucesso = await RealizarTestesIniciais(forcar, token); + + var calAtu = op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo]; + var v = calAtu.Select(x => $"{x.Key} " + (x.Value ? "Sim" : "Não")); + + if (sucesso) + { + Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Teste do módulo {Modulo_ID} finalizado com sucesso: " + string.Join(", ", v)); + + op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Operante, 100, $"Teste do módulo {Modulo_ID} finalizado com sucesso: " + string.Join(", ", v)); + } + else + { + Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v)); + + op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v)); + } + } + catch (OperationCanceledException ex) + { + Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Calibragem cancelada: {ex.Message}"); + op?.Sensoriamento?.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Calibragem do módulo {Modulo_ID} cancelada: {ex.Message}"); + } + catch (Exception ex) + { + Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Erro no teste do módulo {Modulo_ID}: {ex.Message}"); + + op?.Sensoriamento?.InserirLog(Dispositivo, StatusModulo.Falha, 0, $"Erro no teste do módulo {Modulo_ID}: {ex.Message}"); + + throw; + } + finally + { + /* OFF é sempre permitido, inclusive durante emergência. */ + EnviarEstadoSeguroCalibragem(); + lock (_calibragemCtsLock) + { + _calibragemCts?.Dispose(); + _calibragemCts = null; + } + _calibragemGate.Release(); + } + } + public void RequisitarDadosSensoresExternos() @@ -2338,9 +2684,9 @@ namespace AgroBase.Models.Modules if (sensor == null) return; double angulo = sensor.ValoresLeituras?.FirstOrDefault(x => x.posicao == CanMessagePosicaoDados.Dados1 && x.funcao == FuncoesPinout.LeituraAngulo)?.atual?.valor ?? double.NaN; - if (sensor.ID_Num == SensorAnguloBarraEsquerda.ID_Num) + if (sensor.ID_Num == SensorAnguloBarraEsquerda?.ID_Num) AnguloBarraEsquerda = angulo; - else if (sensor.ID_Num == SensorAnguloBarraDireita.ID_Num) + else if (sensor.ID_Num == SensorAnguloBarraDireita?.ID_Num) AnguloBarraDireita = angulo; } break; @@ -2390,6 +2736,17 @@ namespace AgroBase.Models.Modules [JsonIgnore] public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue; + /* + * AdicionarMensagemFila confirma somente enfileiramento. O campo + * legado UltimoComandoEnviado é preservado por compatibilidade, mas + * não é mais atualizado como se a transmissão já tivesse ocorrido. + */ + [JsonIgnore] + public DateTime UltimoComandoEnfileirado { get; set; } = DateTime.MinValue; + + [JsonIgnore] + public DateTime UltimaRespostaPosteriorAoComando { get; set; } = DateTime.MinValue; + [JsonIgnore] public bool UltimoStatusDesejado { get; set; } = false; @@ -2543,6 +2900,8 @@ namespace AgroBase.Models.Modules AnguloControle = AnguloControle, UltimoAnguloDesejado = UltimoAnguloDesejado, UltimoComandoEnviado = UltimoComandoEnviado, + UltimoComandoEnfileirado = UltimoComandoEnfileirado, + UltimaRespostaPosteriorAoComando = UltimaRespostaPosteriorAoComando, UltimoStatusDesejado = UltimoStatusDesejado, Inicializado = Inicializado, Mandatorio = Mandatorio, @@ -2609,7 +2968,7 @@ namespace AgroBase.Models.Modules } public List ValoresLeituras { get; set; } public BombaModoControleModel Controle { get; set; } - public Estado _EstadoLeitura => ValoresLeituras?.FirstOrDefault(x => x.posicao == CanMessagePosicaoDados.Dados1 && x.funcao == FuncoesPinout.EstadoLeitura)?.atual?.valor ?? Estado.Desligado; + public Estado _EstadoLeitura => (Estado)(ValoresLeituras?.FirstOrDefault(x => x.posicao == CanMessagePosicaoDados.Dados1 && x.funcao == FuncoesPinout.EstadoLeitura)?.atual?.valor ?? 0); public double _Potencia => ValoresLeituras?.FirstOrDefault(x => x.posicao == CanMessagePosicaoDados.Dados1 && x.funcao == FuncoesPinout.Potencia)?.atual?.valor ?? 0; public bool _BombaLigada => _Potencia > 0; public double _PressaoSP => ValoresLeituras?.FirstOrDefault(x => x.posicao == CanMessagePosicaoDados.Dados1 && x.funcao == FuncoesPinout.PressaoSP)?.atual?.valor ?? 0; @@ -2680,6 +3039,12 @@ namespace AgroBase.Models.Modules [JsonIgnore] public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue; + [JsonIgnore] + public DateTime UltimoComandoEnfileirado { get; set; } = DateTime.MinValue; + + [JsonIgnore] + public DateTime UltimaRespostaPosteriorAoComando { get; set; } = DateTime.MinValue; + [JsonIgnore] public bool UltimoStatusDesejado { get; set; } = false; @@ -2984,6 +3349,69 @@ namespace AgroBase.Models.Modules return _value; } + private bool FrameValido(byte[] data, S_Code componente, CanMessagePosicaoDados posicao, out string motivo) + { + motivo = null; + + if (data == null || data.Length < 2) + { + motivo = "payload nulo ou menor que 2 bytes"; + return false; + } + + int minimo; + + switch (componente) + { + case S_Code.sMOD: + minimo = posicao == CanMessagePosicaoDados.Status ? 5 : -1; + break; + case S_Code.sTOD: + minimo = posicao == CanMessagePosicaoDados.DadosAll ? 4 : -1; + break; + case S_Code.sBIC: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 4 : -1; + break; + case S_Code.sFLX: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 7 : -1; + break; + case S_Code.sMAS: + case S_Code.sPRS: + case S_Code.sMAG: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 6 : -1; + break; + case S_Code.sBMB: + minimo = posicao == CanMessagePosicaoDados.Status ? 4 : + posicao == CanMessagePosicaoDados.Dados1 ? 8 : + posicao == CanMessagePosicaoDados.Dados2 ? 8 : -1; + break; + case S_Code.sBOT: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 5 : -1; + break; + default: + motivo = $"componente desconhecido ({componente})"; + return false; + } + + if (minimo < 0) + { + motivo = $"posição {posicao} não suportada para {componente}"; + return false; + } + + if (data.Length < minimo) + { + motivo = $"payload curto para {componente}/{posicao}: {data.Length} < {minimo}"; + return false; + } + + return true; + } + private ushort Converter2BytesU16(byte[] data, int idx) { return (ushort)((data[idx] << 8) | data[idx + 1]); @@ -3065,6 +3493,12 @@ namespace AgroBase.Models.Modules public void ProcessarMensagem(CanMessage mensagem) { + if (mensagem?.DataRx == null || mensagem.DataRx.Length < 2) + { + Variaveis.MostrarLog("[ATU/RX] Frame descartado: payload nulo ou incompleto"); + return; + } + CanMessagePosicaoDados posicao = (CanMessagePosicaoDados)mensagem.DataRx[0]; byte ID_Num = mensagem.DataRx[1]; var data = mensagem.DataRx; @@ -3073,252 +3507,277 @@ namespace AgroBase.Models.Modules var Dados = Variaveis.OperacaoEmAndamento.DispAtu.Dados; - //Console.WriteLine($"[ATU] Componente={componente.ToString()}, ID_Num={ID_Num}, Data={FuncoesGlobais.ConverterComandoBytesParaTexto(data)}"); - - switch (componente) + if (!FrameValido(data, componente, posicao, out string motivoFrameInvalido)) { - case S_Code.sMOD: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - T_Code tipoModulo = (T_Code)data[2]; - bool conectado = data[3] == 1; - int versao = data[4]; - - //Console.WriteLine($"[ATU] Resposta {ID_Num} recebida. Tipo={tipoModulo}, Conectado={conectado}, Versão={versao}"); - - Dados.DadosLeitura.Conectado = conectado; - Dados.DadosLeitura.Dispositivo = tipoModulo; - Dados.DadosLeitura.Versao = versao; - break; - } - } - break; - } - case S_Code.sTOD: - { - switch (posicao) - { - case CanMessagePosicaoDados.DadosAll: - { - double latenciaLoop = ConverterByteParaDouble(data, 2) * 100; - Dados.DadosLeitura.LatenciaLoop = Convert.ToInt32(latenciaLoop); - //Console.WriteLine($"[ATU] Ciclo concluido. Latencia de loop: {latenciaLoop} ms"); - break; - } - } - break; - } - case S_Code.sBIC: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - - var bico = Dados.BicosPulverizadores.FirstOrDefault(x => x.ID_Num == ID_Num); - if (bico != null && !Iniciado) - bico.AtualizarEstadoTrecho(false); - - break; - } - case CanMessagePosicaoDados.Dados1: - { - Estado estado = (Estado)data[2]; - int angulo = data[3]; - AtualizarDados(ID_Num, posicao, componente, new List() { estado, angulo }); - break; - } - } - break; - } - case S_Code.sFLX: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - int qtdPulsos = data[2]; - ushort periodo = Converter2BytesU16(data, 3); - double vazao = ConverterByteParaDouble(data, 5); - //Variaveis.MostrarLog($"[AtuadorModel] Vazao: {vazao}, pulsos: {qtdPulsos}, Periodo: {periodo}"); - AtualizarDados(ID_Num, posicao, componente, new List() { qtdPulsos, periodo, vazao }); - break; - } - } - break; - } - case S_Code.sMAS: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - double raw = ConverterByteParaDouble(data, 2); - double massa = ConverterByteParaDouble(data, 4); - //Variaveis.MostrarLog($"[AtuadorModel] Massa: {massa}, Raw: {raw}"); - AtualizarDados(ID_Num, posicao, componente, new List() { raw, massa }); - break; - } - } - break; - } - case S_Code.sPRS: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - ushort raw = Converter2BytesU16(data, 2); - double pressao = ConverterByteParaDouble(data, 4); - AtualizarDados(ID_Num, posicao, componente, new List() { raw, pressao }); - break; - } - } - break; - } - case S_Code.sBMB: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool iniciado = data.Length > 2 && data[2] == 1; - bool malhaFechada = data.Length > 3 && data[3] == 1; - AtualizarDados( - ID_Num, - posicao, - componente, - new List() { - iniciado, - malhaFechada, - } - ); - var bomba = Dados.BombasPressurizadoras?.FirstOrDefault(x => x.ID_Num == ID_Num); - break; - } - case CanMessagePosicaoDados.Dados1: - { - Estado estado = data.Length > 2 ? (Estado)data[2] : Estado.Desligado; - int potenciaSp = data.Length > 3 ? data[3] : 0; - int percentualPotencia = data.Length > 4 ? data[4] : 0; - int pressaoSp = data.Length > 5 ? data[5] : 0; - int pressaoAtual = data.Length > 6 ? data[6] : 0; - ModoControleBomba modo = data.Length > 7 ? (ModoControleBomba)data[7] : ModoControleBomba.OnOff; - AtualizarDados( - ID_Num, - posicao, - componente, - new List() - { - estado, - potenciaSp, - percentualPotencia, - pressaoSp, - pressaoAtual, - modo - } - ); - break; - } - case CanMessagePosicaoDados.Dados2: - { - int qtdBicosLigados = data.Length > 2 ? data[2] : 0; - int potenciaFeedForward = data.Length > 3 ? data[3] : 0; - int potenciaFinal = data.Length > 4 ? data[4] : 0; - int pressaoSensor = data.Length > 5 ? data[5] : 0; - int pressaoSpControle = data.Length > 6 ? data[6] : 0; - byte flags = data.Length > 7 ? data[7] : (byte)0; - AtualizarDados( - ID_Num, - posicao, - componente, - new List() - { - qtdBicosLigados, - potenciaFeedForward, - potenciaFinal, - pressaoSensor, - pressaoSpControle, - flags - } - ); - - break; - } - } - break; - } - case S_Code.sMAG: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - ushort raw = Converter2BytesU16(data, 2); - double angle = ConverterAngle100(data, 4); - AtualizarDados(ID_Num, posicao, componente, new List() { raw, angle }); - //Console.WriteLine($"[sMAG] {ID_Num}, Raw: {raw}, Angulo: {angle}"); - break; - } - } - break; - } - case S_Code.sBOT: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - bool pressionado = data[2] == 1; - bool leitura = data[3] == 1; - bool mudou = data[4] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { pressionado, leitura, mudou }); - break; - } - } - break; - } - + Variaveis.MostrarLog($"[ATU/RX] Frame descartado: ID={ID_Num}; {motivoFrameInvalido}"); + return; } - Dados.DadosLeitura.UltimoComandoRespondido = DateTime.Now; + //Variaveis.MostrarLog($"[ATU] Componente={componente.ToString()}, ID_Num={ID_Num}, Data={FuncoesGlobais.ConverterComandoBytesParaTexto(data)}"); - Variaveis.OperacaoEmAndamento.DispAtu.Dados.AtualizarDadosSensoresExternos(componente, ID_Num); + lock (Dados.EstadoSyncRoot) + { + switch (componente) + { + case S_Code.sMOD: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + T_Code tipoModulo = (T_Code)data[2]; + bool conectado = data[3] == 1; + int versao = data[4]; + + //Variaveis.MostrarLog($"[ATU] Resposta {ID_Num} recebida. Tipo={tipoModulo}, Conectado={conectado}, Versão={versao}"); + + Dados.DadosLeitura.Conectado = conectado; + Dados.DadosLeitura.Dispositivo = tipoModulo; + Dados.DadosLeitura.Versao = versao; + + if (conectado) + Dados.ConfirmarMapeamentoAposDescoberta(); + break; + } + } + break; + } + case S_Code.sTOD: + { + switch (posicao) + { + case CanMessagePosicaoDados.DadosAll: + { + double latenciaLoop = ConverterByteParaDouble(data, 2) * 100; + Dados.DadosLeitura.LatenciaLoop = Convert.ToInt32(latenciaLoop); + Dados.AtuPronto = true; + //Variaveis.MostrarLog($"[ATU] Ciclo concluido. Latencia de loop: {latenciaLoop} ms"); + break; + } + } + break; + } + case S_Code.sBIC: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); + + var bico = Dados.BicosPulverizadores.FirstOrDefault(x => x.ID_Num == ID_Num); + if (bico != null && !Iniciado) + bico.AtualizarEstadoTrecho(false); + + break; + } + case CanMessagePosicaoDados.Dados1: + { + Estado estado = (Estado)data[2]; + int angulo = data[3]; + AtualizarDados(ID_Num, posicao, componente, new List() { estado, angulo }); + break; + } + } + break; + } + case S_Code.sFLX: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + int qtdPulsos = data[2]; + ushort periodo = Converter2BytesU16(data, 3); + double vazao = ConverterByteParaDouble(data, 5); + //Variaveis.MostrarLog($"[AtuadorModel] Vazao: {vazao}, pulsos: {qtdPulsos}, Periodo: {periodo}"); + AtualizarDados(ID_Num, posicao, componente, new List() { qtdPulsos, periodo, vazao }); + break; + } + } + break; + } + case S_Code.sMAS: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + double raw = ConverterByteParaDouble(data, 2); + double massa = ConverterByteParaDouble(data, 4); + //Variaveis.MostrarLog($"[AtuadorModel] Massa: {massa}, Raw: {raw}"); + AtualizarDados(ID_Num, posicao, componente, new List() { raw, massa }); + break; + } + } + break; + } + case S_Code.sPRS: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + ushort raw = Converter2BytesU16(data, 2); + double pressao = ConverterByteParaDouble(data, 4); + AtualizarDados(ID_Num, posicao, componente, new List() { raw, pressao }); + break; + } + } + break; + } + case S_Code.sBMB: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool iniciado = data.Length > 2 && data[2] == 1; + bool malhaFechada = data.Length > 3 && data[3] == 1; + AtualizarDados( + ID_Num, + posicao, + componente, + new List() { + iniciado, + malhaFechada, + } + ); + var bomba = Dados.BombasPressurizadoras?.FirstOrDefault(x => x.ID_Num == ID_Num); + break; + } + case CanMessagePosicaoDados.Dados1: + { + Estado estado = data.Length > 2 ? (Estado)data[2] : Estado.Desligado; + int potenciaSp = data.Length > 3 ? data[3] : 0; + int percentualPotencia = data.Length > 4 ? data[4] : 0; + int pressaoSp = data.Length > 5 ? data[5] : 0; + int pressaoAtual = data.Length > 6 ? data[6] : 0; + ModoControleBomba modo = data.Length > 7 ? (ModoControleBomba)data[7] : ModoControleBomba.OnOff; + AtualizarDados( + ID_Num, + posicao, + componente, + new List() + { + estado, + potenciaSp, + percentualPotencia, + pressaoSp, + pressaoAtual, + modo + } + ); + break; + } + case CanMessagePosicaoDados.Dados2: + { + int qtdBicosLigados = data.Length > 2 ? data[2] : 0; + int potenciaFeedForward = data.Length > 3 ? data[3] : 0; + int potenciaFinal = data.Length > 4 ? data[4] : 0; + int pressaoSensor = data.Length > 5 ? data[5] : 0; + int pressaoSpControle = data.Length > 6 ? data[6] : 0; + byte flags = data.Length > 7 ? data[7] : (byte)0; + AtualizarDados( + ID_Num, + posicao, + componente, + new List() + { + qtdBicosLigados, + potenciaFeedForward, + potenciaFinal, + pressaoSensor, + pressaoSpControle, + flags + } + ); + + break; + } + } + break; + } + case S_Code.sMAG: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + ushort raw = Converter2BytesU16(data, 2); + double angle = ConverterAngle100(data, 4); + AtualizarDados(ID_Num, posicao, componente, new List() { raw, angle }); + //Variaveis.MostrarLog($"[sMAG] {ID_Num}, Raw: {raw}, Angulo: {angle}"); + break; + } + } + break; + } + case S_Code.sBOT: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + bool pressionado = data[2] == 1; + bool leitura = data[3] == 1; + bool mudou = data[4] == 1; + AtualizarDados(ID_Num, posicao, componente, new List() { pressionado, leitura, mudou }); + break; + } + } + break; + } + + } + + DateTime respostaEm = DateTime.Now; + Dados.DadosLeitura.UltimoComandoRespondido = respostaEm; + + if (componente == S_Code.sMOD && Dados.DadosLeitura.Conectado) + Dados.SincronizarEstadoSeguroAposReconexao(); + + var bicoRespondido = Dados.BicosPulverizadores?.FirstOrDefault(x => x.ID_Num == ID_Num); + if (bicoRespondido != null && bicoRespondido.UltimoComandoEnfileirado != DateTime.MinValue && respostaEm >= bicoRespondido.UltimoComandoEnfileirado) + bicoRespondido.UltimaRespostaPosteriorAoComando = respostaEm; + + var bombaRespondida = Dados.BombasPressurizadoras?.FirstOrDefault(x => x.ID_Num == ID_Num); + if (bombaRespondida != null && bombaRespondida.UltimoComandoEnfileirado != DateTime.MinValue && respostaEm >= bombaRespondida.UltimoComandoEnfileirado) + bombaRespondida.UltimaRespostaPosteriorAoComando = respostaEm; + + Variaveis.OperacaoEmAndamento.DispAtu.Dados.AtualizarDadosSensoresExternos(componente, ID_Num); + } } } diff --git a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs index b2beb47b9..9ccba5436 100644 --- a/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs +++ b/AgroBase/AgroBase/Models/Modules/SensoriamentoModel.cs @@ -1,11 +1,10 @@ -using AgroBase.Services; +using AgroBase.Services; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using static AgroBase.Models.Enums; using System.Globalization; -using System.Data; using Newtonsoft.Json; namespace AgroBase.Models.Modules @@ -15,33 +14,74 @@ namespace AgroBase.Models.Modules public T_Code Dispositivo { get; set; } = T_Code.Sen; public string Modulo_ID { get; set; } = T_Code.Sen.ToString(); public byte _EnderecoCAN_Tx { get; set; } = 0x11; - public byte _EnderecoCAN_Rx { get; set; } = 0x12; + public byte _EnderecoCAN_Rx { get; set; } = 0x11; public int _TaxaAmostragem { get; set; } = 1000; public double FrequenciaBase { get { - return 1.0 / (_TaxaAmostragem / 1000.0); + return _TaxaAmostragem > 0 + ? 1000.0 / _TaxaAmostragem + : 0.0; } } public bool RequisitarDados { get; set; } = true; public static int TempoLimiteConexao { get; set; } = 8000; + private const int IntervaloDescobertaDesconectadoMs = 2000; + private const int IntervaloReenvioComandoMs = 1500; + private readonly object _estadoLock = new object(); + private readonly object _registradorLock = new object(); + private DateTime _ultimaDescobertaEnviada = DateTime.MinValue; + private DateTime _ultimaRespostaDescoberta = DateTime.MinValue; + + [JsonIgnore] + internal object EstadoSyncRoot => _estadoLock; + + [JsonIgnore] + private bool ModeloRuntimeAtivo => + ReferenceEquals(Variaveis.OperacaoEmAndamento?.DispSen?.Dados, this); + public bool Conectado { get { - DateTime Agora = DateTime.Now; - bool modConectado = DadosLeitura.UltimoComandoRespondido.AddMilliseconds(TempoLimiteConexao) > Agora; - return modConectado; + lock (_estadoLock) + { + DateTime agora = DateTime.Now; + return DadosLeitura != null && + DadosLeitura.UltimoComandoRespondido != DateTime.MinValue && + DadosLeitura.UltimoComandoRespondido.AddMilliseconds(Math.Max(1, TempoLimiteConexao)) > agora; + } } } - public bool ConexaoAtiva + + [JsonIgnore] + public bool TelemetriaRecente => Conectado; + + [JsonIgnore] + public bool FirmwareDeclarouConectado { get { - return Variaveis.OperacaoEmAndamento.Sensoriamento?.OperadorSaude?.ModulosSaude?.Any(x => x.modulo == Dispositivo && x.status == StatusModulo.Operante) ?? false; + lock (_estadoLock) + return DadosLeitura?.Conectado == true; } } + + [JsonIgnore] + internal bool ModuloIdentificado + { + get + { + lock (_estadoLock) + { + return _ultimaRespostaDescoberta != DateTime.MinValue && + DadosLeitura?.Dispositivo == Dispositivo; + } + } + } + + public bool ConexaoAtiva => TelemetriaRecente; public SensoriamentoMensagemJsonModel DadosLeitura { get; set; } = new SensoriamentoMensagemJsonModel(); public static PinoutDataModel Pinout { get; set; } public static List Funcoes { get; set; } = new List() @@ -56,7 +96,7 @@ namespace AgroBase.Models.Modules public UltrasonicA05Configuracoes ConfiguracaoSensorUltrassom { get; set; } [JsonIgnore] - private SenHandler _SenHandler = new SenHandler(); + private readonly SenHandler _SenHandler = new SenHandler(); public CoulombCounterService ContadorCarga = new CoulombCounterService(VariaveisEquipamento.CorrenteMaximaBateria); public CoolerControlService CoolerControl = new CoolerControlService(); @@ -2025,55 +2065,211 @@ namespace AgroBase.Models.Modules }, }; + obj.ValidarConfiguracaoProduto(); return obj; } + private void ValidarConfiguracaoProduto() + { + if (Sensores == null || Sinaleiros == null || Reles == null || Servos == null) + throw new InvalidOperationException("Cadastro do sensoriamento está incompleto."); + + var componentes = new List<(string tipo, string id, int idNum, S_Code codigo, List leituras)>(); + + componentes.AddRange((Sensores ?? new List()) + .Where(x => x != null) + .Select(x => ("Sensor", x.ID, x.ID_Num, x.Componente, x.ValoresLeituras))); + componentes.AddRange((Sinaleiros ?? new List()) + .Where(x => x != null) + .Select(x => ("Sinaleiro", x.ID, x.ID_Num, x.Componente, x.ValoresLeituras))); + componentes.AddRange((Reles ?? new List()) + .Where(x => x != null) + .Select(x => ("Relé", x.ID, x.ID_Num, x.Componente, x.ValoresLeituras))); + componentes.AddRange((Servos ?? new List()) + .Where(x => x != null) + .Select(x => ("Servo", x.ID, x.ID_Num, x.Componente, x.ValoresLeituras))); + + var idsReservados = new HashSet() + { + Variaveis.ID_Num_sMOD, + Variaveis.ID_Num_sTOD, + }; + + var idsForaDoProtocolo = componentes + .Where(x => x.idNum < byte.MinValue || x.idNum > byte.MaxValue || idsReservados.Contains(x.idNum)) + .Select(x => x.idNum) + .Distinct() + .ToList(); + + if (idsForaDoProtocolo.Count > 0) + throw new InvalidOperationException( + "Sensoriamento possui ID_Num fora do intervalo CAN ou reservado: " + + string.Join(", ", idsForaDoProtocolo) + ); + + var idsNumericosDuplicados = componentes + .GroupBy(x => x.idNum) + .Where(x => x.Count() > 1) + .Select(x => x.Key) + .ToList(); + + if (idsNumericosDuplicados.Count > 0) + throw new InvalidOperationException( + "Sensoriamento possui ID_Num duplicado(s): " + string.Join(", ", idsNumericosDuplicados) + ); + + var idsDuplicados = componentes + .Where(x => !string.IsNullOrWhiteSpace(x.id)) + .GroupBy(x => x.id, StringComparer.OrdinalIgnoreCase) + .Where(x => x.Count() > 1) + .Select(x => x.Key) + .ToList(); + + if (idsDuplicados.Count > 0) + throw new InvalidOperationException( + "Sensoriamento possui ID duplicado(s): " + string.Join(", ", idsDuplicados) + ); + + var codigosSensoresSuportados = new HashSet() + { + S_Code.sNTC, + S_Code.sCOR, + S_Code.sIMU, + S_Code.sGAS, + S_Code.sSHT, + S_Code.sLUZ, + S_Code.sIUV, + S_Code.sQAR, + S_Code.sCHV, + S_Code.sMAG, + }; + + foreach (var componente in componentes) + { + if (string.IsNullOrWhiteSpace(componente.id)) + throw new InvalidOperationException($"{componente.tipo} com ID_Num={componente.idNum} não possui ID textual."); + + bool codigoValido = + (componente.tipo == "Sensor" && codigosSensoresSuportados.Contains(componente.codigo)) || + (componente.tipo == "Sinaleiro" && componente.codigo == S_Code.sLED) || + (componente.tipo == "Relé" && componente.codigo == S_Code.sRLE) || + (componente.tipo == "Servo" && componente.codigo == S_Code.sSRV); + + if (!codigoValido) + throw new InvalidOperationException( + $"{componente.tipo} {componente.id} possui código incompatível: {componente.codigo}." + ); + + if (componente.leituras == null || componente.leituras.Count == 0) + throw new InvalidOperationException($"{componente.tipo} {componente.id} não possui ValoresLeituras."); + + if (componente.leituras.Any(x => x == null)) + throw new InvalidOperationException($"{componente.tipo} {componente.id} possui leitura nula no cadastro."); + + foreach (var grupo in componente.leituras.Where(x => x != null).GroupBy(x => x.posicao)) + { + var indices = grupo.Select(x => x.indice).OrderBy(x => x).ToList(); + var esperados = Enumerable.Range(0, indices.Count).ToList(); + + if (!indices.SequenceEqual(esperados)) + { + throw new InvalidOperationException( + $"{componente.tipo} {componente.id} possui índices inválidos em {grupo.Key}: " + + string.Join(", ", indices) + ); + } + } + } + } + public void LimparDados(T _dados) { var Dados = _dados as SensoriamentoModel; - Dados.DadosLeitura = new SensoriamentoMensagemJsonModel(); - Dados.Reles.ForEach(x => - { - x.StatusControle = Estado.Desligado; - }); - Dados.Servos.ForEach(x => - { - x._AnguloControle = x.Incremental ? 0 : 180; - }); - ReiniciarLeituras(Dados); + if (Dados == null) + return; + + /* + * O início de uma operação não representa perda física do SEN. + * Preservamos conexão, inicialização, telemetria e timestamps para + * impedir um snapshot falso de módulo/componentes desconectados. + * Somente as intenções de controle retornam a um estado seguro. + */ + lock (Dados.EstadoSyncRoot) + { + Dados.Reles?.Where(x => x != null).ToList().ForEach(x => + { + x.StatusControle = Estado.Desligado; + x.UltimoEstadoDesejado = Estado.Desligado; + x.UltimoComandoEnviado = DateTime.MinValue; + }); + + Dados.Servos?.Where(x => x != null).ToList().ForEach(x => + { + x._AnguloControle = x.AnguloNeutro; + x.UltimoAnguloDesejado = x.AnguloNeutro; + x.UltimoComandoEnviado = DateTime.MinValue; + }); + + Dados.Sinaleiros?.Where(x => x != null).ToList().ForEach(x => + { + x.Comportamento = VariaveisEquipamento.ComportamentoLedPorStatus(StatusLED.Apagado); + x.UltimoComportamentoDesejado = null; + x.UltimoComandoEnviado = DateTime.MinValue; + }); + + } } public void ReiniciarLeituras(SensoriamentoModel Dados) { - Dados.Reles.ForEach(x => + ReiniciarLeituras(Dados, apagarValoresLeituras: true); + } + + public void ReiniciarLeituras(SensoriamentoModel Dados, bool apagarValoresLeituras) + { + if (Dados == null || !apagarValoresLeituras) + return; + + lock (Dados.EstadoSyncRoot) { - x.ValoresLeituras.ForEach(l => + foreach (var rele in Dados.Reles ?? new List()) { - l.atual = new CanCommandFreqModel(); - }); - }); - Dados.Sinaleiros.ForEach(x => - { - x.ValoresLeituras.ForEach(l => + if (rele == null) continue; + rele.Inicializado = false; + ReiniciarValores(rele.ValoresLeituras); + } + + foreach (var sinaleiro in Dados.Sinaleiros ?? new List()) { - l.atual = new CanCommandFreqModel(); - }); - }); - Dados.Sensores.ForEach(x => - { - x.ValoresLeituras.ForEach(l => + if (sinaleiro == null) continue; + sinaleiro.Inicializado = false; + ReiniciarValores(sinaleiro.ValoresLeituras); + } + + foreach (var sensor in Dados.Sensores ?? new List()) { - l.atual = new CanCommandFreqModel(); - }); - }); - Dados.Servos.ForEach(x => - { - x.ValoresLeituras.ForEach(l => + if (sensor == null) continue; + sensor.Inicializado = false; + ReiniciarValores(sensor.ValoresLeituras); + } + + foreach (var servo in Dados.Servos ?? new List()) { - l.atual = new CanCommandFreqModel(); - }); - }); + if (servo == null) continue; + servo.Inicializado = false; + ReiniciarValores(servo.ValoresLeituras); + } + } + } + + private static void ReiniciarValores(IEnumerable valores) + { + if (valores == null) + return; + + foreach (var leitura in valores.Where(x => x != null)) + leitura.atual = new CanCommandFreqModel(); } public async Task VerificaDispositivoConectado() @@ -2082,17 +2278,22 @@ namespace AgroBase.Models.Modules if (!CanManager.CanService.IsConnected) return false; - if (op.DispSen == null) return false; + if (op?.DispSen == null || !ReferenceEquals(op.DispSen.Dados, this)) return false; CanManager.CanService.RegistrarHandler(_EnderecoCAN_Rx, _SenHandler); + DateTime Inicio = DateTime.Now; RequisitarDadosDescoberta(); - DateTime Inicio = DateTime.Now; - bool Respondido() => op.DispSen.Dados.DadosLeitura.UltimoComandoRespondido > Inicio; + bool Respondido() + { + lock (_estadoLock) + return _ultimaRespostaDescoberta > Inicio; + } + await FuncoesGlobais.AguardarCondicaoAsync(Respondido, 500, 50); - if (Conectado) + if (Respondido()) { DefinirDispositivo(); op.DispSen?.EnviarProtocolosConfiguracao(); @@ -2105,31 +2306,103 @@ namespace AgroBase.Models.Modules { var op = Variaveis.OperacaoEmAndamento; - if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == DadosLeitura.Dispositivo && x.Mod_ID == Modulo_ID)) - { - SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel() - { - Endereco = CanManager.CanService._portName, - Dispositivo = DadosLeitura.Dispositivo, - Versao = DadosLeitura.Versao.ToString(), - Mod_ID = Modulo_ID, - }); + if (op?.Sensoriamento?.Operacao == null || !ReferenceEquals(op.DispSen?.Dados, this)) + return; - if (op.Sensoriamento?.Operacao?.ModsCalibragem == null) - op.Sensoriamento.Operacao.ModsCalibragem = new Dictionary>(); - op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo] = new Dictionary(); - op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Conectado, 0, "Dispositivo conectado"); + T_Code dispositivoDeclarado; + int versao; + + lock (_estadoLock) + { + dispositivoDeclarado = DadosLeitura?.Dispositivo ?? default(T_Code); + versao = DadosLeitura?.Versao ?? 0; } + + if (dispositivoDeclarado != Dispositivo) + { + Variaveis.MostrarLog( + $"[SEN/DESCOBERTA] Resposta ignorada: esperado={Dispositivo}, recebido={dispositivoDeclarado}" + ); + return; + } + + bool novoMapeamento = false; + + lock (SerialService.DispositivosMapeados) + { + if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo && x.Mod_ID == Modulo_ID)) + return; + + SerialService.AdicionarOuAtualizarDispositivoMapeado(new DispositivoDetalhesModel() + { + Endereco = CanManager.CanService._portName, + Dispositivo = dispositivoDeclarado, + Versao = versao.ToString(), + Mod_ID = Modulo_ID, + }); + + novoMapeamento = true; + } + + /* Uma redescoberta não pode apagar o estado já consolidado. */ + if (op.Sensoriamento.Operacao.ModsCalibragem == null) + op.Sensoriamento.Operacao.ModsCalibragem = new Dictionary>(); + + if (!op.Sensoriamento.Operacao.ModsCalibragem.ContainsKey(Dispositivo)) + op.Sensoriamento.Operacao.ModsCalibragem[Dispositivo] = new Dictionary(); + + if (novoMapeamento) + op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Conectado, 0, "Dispositivo conectado"); + } + + internal void ConfirmarMapeamentoAposDescoberta(DateTime respostaEm) + { + lock (_estadoLock) + _ultimaRespostaDescoberta = respostaEm; + + DefinirDispositivo(); } private void RequisitarDadosDescoberta() { - Variaveis.OperacaoEmAndamento.DispSen?.AdicionarMensagemFila(Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Status, CanMessagePosicaoDados.Status, null, true, false, true); + var dispSen = Variaveis.OperacaoEmAndamento?.DispSen; + + if (dispSen == null || !ReferenceEquals(dispSen.Dados, this)) + return; + + lock (_estadoLock) + _ultimaDescobertaEnviada = DateTime.Now; + + dispSen.AdicionarMensagemFila(Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Status, CanMessagePosicaoDados.Status, null, true, false, true); + } + + private void ManterDescobertaAtiva(DateTime agora) + { + DateTime ultimaDescoberta; + bool identificado; + + lock (_estadoLock) + { + ultimaDescoberta = _ultimaDescobertaEnviada; + identificado = _ultimaRespostaDescoberta != DateTime.MinValue && + DadosLeitura?.Dispositivo == Dispositivo; + } + + if (Conectado && identificado) + return; + + if ((agora - ultimaDescoberta).TotalMilliseconds < IntervaloDescobertaDesconectadoMs) + return; + + RequisitarDadosDescoberta(); } public void RequisitarDadosModulo(int ID_Num, CanMessagePosicaoDados posicaoTx, CanMessagePosicaoDados posicaoRx, bool aguardaResposta) { - Variaveis.OperacaoEmAndamento.DispSen?.AdicionarMensagemFila(ID_Num, posicaoTx, posicaoRx, null, aguardaResposta); + if (!ModeloRuntimeAtivo) + return; + + Variaveis.OperacaoEmAndamento?.DispSen?.AdicionarMensagemFila(ID_Num, posicaoTx, posicaoRx, null, aguardaResposta); } public (int, CanMessagePosicaoDados, byte[]) ProtocoloComandoReset() @@ -2146,166 +2419,195 @@ namespace AgroBase.Models.Modules public List<(int, CanMessagePosicaoDados, byte[])> ProtocoloConfiguracao(bool Conectar) { - // Inicializar o dicionário - List<(int, CanMessagePosicaoDados, byte[])> config = new List<(int, CanMessagePosicaoDados, byte[])>(); - - if (!DadosLeitura.Conectado) + lock (_estadoLock) { - // Adicionar configuração do ProtocoloMOD - var protocoloMOD = ModConfig(Conectar); - config.AddRange(protocoloMOD); + var config = new List<(int, CanMessagePosicaoDados, byte[])>(); + bool firmwareConectado = DadosLeitura?.Conectado == true; + + if (!firmwareConectado) + config.AddRange(ModConfig(Conectar)); + + bool configurarTodos = !firmwareConectado && Conectar; + + /* + * Reconfigura todos após a conexão do módulo e, durante a + * operação, somente componentes ausentes ou indevidamente + * ativos. Isso permite recuperar um canal sem resetar o SEN. + */ + config.AddRange(SinaleirosConfig(Conectar, configurarTodos)); + config.AddRange(ServosConfig(Conectar, configurarTodos)); + config.AddRange(RelesConfig(Conectar, configurarTodos)); + config.AddRange(SensoresConfig(Conectar, configurarTodos)); + + return config; } - - bool configurarTodos = !DadosLeitura.Conectado && Conectar; - - // Adicionar configurações dos Sinaleiros - //var sinaleirosConfig = SinaleirosConfig(Conectar, configurarTodos); - //config.AddRange(sinaleirosConfig); - - // Adicionar configurações dos Servos - //var servosConfig = ServosConfig(Conectar, configurarTodos); - //config.AddRange(servosConfig); - - // Adicionar configurações dos Reles - //var relesConfig = RelesConfig(Conectar, configurarTodos); - //config.AddRange(relesConfig); - - // Adicionar configurações dos Sensores que devem ser aferidos - //var sensoresConfig = SensoresConfig(Conectar, configurarTodos); - //config.AddRange(sensoresConfig); - - return config; } private List<(int, CanMessagePosicaoDados, byte[])> ModConfig(bool Conectar) { + int periodoSegundos = Math.Max( + 1, + Math.Min(byte.MaxValue, _TaxaAmostragem / 1000) + ); + var config1 = new List { (byte)(Conectar ? 1 : 0), - (byte)(_TaxaAmostragem / 1000), + (byte)periodoSegundos, (byte)(RequisitarDados ? 1 : 0), - (byte)0x00, //LoRaParametrosBase.address + + // Campo reservado do protocolo. + // Antigamente era LoRaParametrosBase.address. + // A firmware ainda espera esta posição antes de SDA/SCL. + 0x00, }; - config1.AddRange(PinoutModel.PinoProtocolo(Pinout, Funcoes, false)); + if (Pinout != null) + { + byte[] pinout = + PinoutModel.PinoProtocolo(Pinout, Funcoes, false); + + if (pinout != null) + config1.AddRange(pinout); + } return new List<(int, CanMessagePosicaoDados, byte[])>() { - (Variaveis.ID_Num_sMOD, CanMessagePosicaoDados.Config1, config1.ToArray()), + ( + Variaveis.ID_Num_sMOD, + CanMessagePosicaoDados.Config1, + config1.ToArray() + ), }; } public List<(int, CanMessagePosicaoDados, byte[])> SensoresConfig(bool Conectar, bool ConfigurarTodos) { - var config = new List<(int, CanMessagePosicaoDados, byte[])>(); - - foreach (var sensor in Sensores.Where(x => (x.Aferir && (ConfigurarTodos ? true : x.Inicializado != Conectar)) || (!x.Aferir && x.Inicializado))) + lock (_estadoLock) { - if (!sensor.Aferir && sensor.Inicializado) - { - Conectar = false; - } - (CanMessagePosicaoDados posicao, byte[] payload) = sensor.ProtocoloConfiguracao(Pinout, Conectar, Dispositivo); - if (payload != null) - { + var config = new List<(int, CanMessagePosicaoDados, byte[])>(); - config.Add((sensor.ID_Num, posicao, payload)); + foreach (var sensor in (Sensores ?? new List()) + .Where(x => x != null && ((x.Aferir && (ConfigurarTodos || x.Inicializado != Conectar)) || (!x.Aferir && x.Inicializado)))) + { + bool conectarComponente = Conectar && sensor.Aferir; + (CanMessagePosicaoDados posicao, byte[] payload) = sensor.ProtocoloConfiguracao(Pinout, conectarComponente, Dispositivo); + + if (payload != null) + config.Add((sensor.ID_Num, posicao, payload)); } + + return config; } - - return config; } private List<(int, CanMessagePosicaoDados, byte[])> SinaleirosConfig(bool Conectar, bool ConfigurarTodos) { - var config = new List<(int, CanMessagePosicaoDados, byte[])>(); - - foreach (var sinaleiro in Sinaleiros.Where(x => (x.Controlar && (ConfigurarTodos ? true : x.Inicializado != Conectar)) || (!x.Controlar && x.Inicializado))) + lock (_estadoLock) { - if (!sinaleiro.Controlar && sinaleiro.Inicializado) - { - Conectar = false; - } - (CanMessagePosicaoDados posicao, var payload) = sinaleiro.ProtocoloConfiguracao(Pinout, Conectar); - if (payload != null) - { - config.Add((sinaleiro.ID_Num, posicao, payload)); - } - } + var config = new List<(int, CanMessagePosicaoDados, byte[])>(); - return config; + foreach (var sinaleiro in (Sinaleiros ?? new List()) + .Where(x => x != null && ((x.Controlar && (ConfigurarTodos || x.Inicializado != Conectar)) || (!x.Controlar && x.Inicializado)))) + { + bool conectarComponente = Conectar && sinaleiro.Controlar; + (CanMessagePosicaoDados posicao, byte[] payload) = sinaleiro.ProtocoloConfiguracao(Pinout, conectarComponente); + + if (payload != null) + config.Add((sinaleiro.ID_Num, posicao, payload)); + } + + return config; + } } private List<(int, CanMessagePosicaoDados, byte[])> RelesConfig(bool Conectar, bool ConfigurarTodos) { - var config = new List<(int, CanMessagePosicaoDados, byte[])>(); - - foreach (var rele in Reles.Where(x => (x.Controlar && (ConfigurarTodos ? true : x.Inicializado != Conectar)) || (!x.Controlar && x.Inicializado))) + lock (_estadoLock) { - if (!rele.Controlar && rele.Inicializado) - { - Conectar = false; - } - (CanMessagePosicaoDados posicao, var payload) = rele.ProtocoloConfiguracao(Pinout, Conectar); - if (payload != null) - { - config.Add((rele.ID_Num, posicao, payload)); - } - } + var config = new List<(int, CanMessagePosicaoDados, byte[])>(); - return config; + foreach (var rele in (Reles ?? new List()) + .Where(x => x != null && ((x.Controlar && (ConfigurarTodos || x.Inicializado != Conectar)) || (!x.Controlar && x.Inicializado)))) + { + bool conectarComponente = Conectar && rele.Controlar; + (CanMessagePosicaoDados posicao, byte[] payload) = rele.ProtocoloConfiguracao(Pinout, conectarComponente); + + if (payload != null) + config.Add((rele.ID_Num, posicao, payload)); + } + + return config; + } } private List<(int, CanMessagePosicaoDados, byte[])> ServosConfig(bool Conectar, bool ConfigurarTodos) { - var config = new List<(int, CanMessagePosicaoDados, byte[])>(); - - foreach (var servo in Servos.Where(x => (x.Controlar && (ConfigurarTodos ? true : x.Inicializado != Conectar)) || (!x.Controlar && x.Inicializado))) + lock (_estadoLock) { - if (!servo.Controlar && servo.Inicializado) - { - Conectar = false; - } - (CanMessagePosicaoDados posicao, var payload) = servo.ProtocoloConfiguracao(Pinout, Conectar); - if (payload != null) - { - config.Add((servo.ID_Num, posicao, payload)); - } - } + var config = new List<(int, CanMessagePosicaoDados, byte[])>(); - return config; + foreach (var servo in (Servos ?? new List()) + .Where(x => x != null && ((x.Controlar && (ConfigurarTodos || x.Inicializado != Conectar)) || (!x.Controlar && x.Inicializado)))) + { + bool conectarComponente = Conectar && servo.Controlar; + (CanMessagePosicaoDados posicao, byte[] payload) = servo.ProtocoloConfiguracao(Pinout, conectarComponente); + + if (payload != null) + config.Add((servo.ID_Num, posicao, payload)); + } + + return config; + } } public void IniciarRegistrador() { - if (!(tmrRegistrador?.IsRunning ?? false)) + ValidarConfiguracaoProduto(); + + lock (_registradorLock) { + if (tmrRegistrador?.IsRunning ?? false) + return; + LimparDados(this); - PararRegistrador(); + var timerAnterior = tmrRegistrador; + tmrRegistrador = null; + timerAnterior?.Dispose(); - tmrRegistrador = new AsyncTaskTimerModel("tmrRegistrador_" + T_Code.Sen.ToString(), tmrRegistrador_Tick, _TaxaAmostragem); + int intervaloSeguro = Math.Max(1, _TaxaAmostragem); + tmrRegistrador = new AsyncTaskTimerModel("tmrRegistrador_" + T_Code.Sen.ToString(), tmrRegistrador_Tick, intervaloSeguro); tmrRegistrador.Start(); } } public void PararRegistrador() { - tmrRegistrador?.Dispose(); + AsyncTaskTimerModel timer; + + lock (_registradorLock) + { + timer = tmrRegistrador; + tmrRegistrador = null; + } + + timer?.Dispose(); } public async Task tmrRegistrador_Tick() { var op = Variaveis.OperacaoEmAndamento; - if (Variaveis.Fechando) return; + if (Variaveis.Fechando || op?.DispSen == null || !ReferenceEquals(op.DispSen.Dados, this)) + return; - tmrRegistrador.SetInterval(_TaxaAmostragem); + tmrRegistrador?.SetInterval(Math.Max(1, _TaxaAmostragem)); - if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Sen) && (_VezesLeituraReconexao >= _TempoReconexaoComponentes)) + if (SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo) && (_VezesLeituraReconexao >= _TempoReconexaoComponentes)) { - op.DispSen?.EnviarProtocolosConfiguracao(); - + op.DispAtu?.EnviarProtocolosConfiguracao(); + _VezesLeituraReconexao = 0; } else @@ -2313,87 +2615,204 @@ namespace AgroBase.Models.Modules _VezesLeituraReconexao++; } - if (DadosLeitura.Conectado) + AtualizaComponentesIniciados(); + ManterDescobertaAtiva(DateTime.Now); + + if (!ModeloRuntimeAtivo) + return; + + if (Conectado) { if (RequisitarDados) - { RequisitarDadosModulo(Variaveis.ID_Num_sTOD, CanMessagePosicaoDados.DadosAll, CanMessagePosicaoDados.DadosAll, true); - } + VerificaControleLeitura(); } - AtualizaComponentesIniciados(); + await Task.CompletedTask; } private void VerificaControleLeitura() { - if (Variaveis.Fechando) return; + if (Variaveis.Fechando || !ModeloRuntimeAtivo || !Conectado || !FirmwareDeclarouConectado || !ModuloIdentificado) + return; - foreach (var sinaleiro in Sinaleiros.Where(x => x.Inicializado)) + DateTime agora = DateTime.Now; + + lock (_estadoLock) { - SensorSinaleiroComportamentoModel comportamentoDesejado = sinaleiro.Controlar ? sinaleiro.Comportamento : VariaveisEquipamento.ComportamentoLedPorStatus(); - bool comportamentoDivergente = comportamentoDesejado.statusLED != sinaleiro._StatusLed || comportamentoDesejado.id != sinaleiro._StatusLedId; - if (comportamentoDivergente) + foreach (var sinaleiro in (Sinaleiros ?? new List()) + .Where(x => x != null && x.Inicializado)) { - GeneralJoystick.EnviarComandoSensoriamento(sinaleiro.Componente, sinaleiro.ID, comportamentoDesejado); + SensorSinaleiroComportamentoModel comportamentoDesejado = + sinaleiro.Controlar + ? sinaleiro.Comportamento + : VariaveisEquipamento.ComportamentoLedPorStatus(); + + if (comportamentoDesejado == null) + comportamentoDesejado = VariaveisEquipamento.ComportamentoLedPorStatus(); + + bool comportamentoDivergente = + comportamentoDesejado.statusLED != sinaleiro._StatusLed || + comportamentoDesejado.id != sinaleiro._StatusLedId; + + bool desejoMudou = sinaleiro.UltimoComportamentoDesejado != comportamentoDesejado; + + if ((comportamentoDivergente || desejoMudou) && + PodeReenviarComando(sinaleiro.UltimoComandoEnviado, desejoMudou, agora)) + { + if (TentarEnviarComando( + () => GeneralJoystick.EnviarComandoSensoriamento(sinaleiro.Componente, sinaleiro.ID, comportamentoDesejado), + sinaleiro.ID)) + { + sinaleiro.UltimoComandoEnviado = agora; + sinaleiro.UltimoComportamentoDesejado = CopiarComportamento(comportamentoDesejado); + } + } + } + + foreach (var rele in (Reles ?? new List()) + .Where(x => x != null && x.Inicializado)) + { + Estado estadoDesejado = + rele.Controlar && rele.StatusControle == Estado.Ligado + ? Estado.Ligado + : Estado.Desligado; + + bool estadoDivergente = estadoDesejado != rele._EstadoLeitura; + bool desejoMudou = estadoDesejado != rele.UltimoEstadoDesejado; + + if ((estadoDivergente || desejoMudou) && + PodeReenviarComando(rele.UltimoComandoEnviado, desejoMudou, agora)) + { + if (TentarEnviarComando( + () => GeneralJoystick.EnviarComandoSensoriamento(rele.Componente, rele.ID, estadoDesejado), + rele.ID)) + { + rele.UltimoComandoEnviado = agora; + rele.UltimoEstadoDesejado = estadoDesejado; + } + } + } + + foreach (var servo in (Servos ?? new List()) + .Where(x => x != null && x.Inicializado)) + { + double anguloDesejado = servo.Controlar ? servo._AnguloControle : servo.AnguloNeutro; + bool anguloDivergente = Math.Abs(anguloDesejado - servo._AnguloLeiutra) > 1.0; + bool desejoMudou = Math.Abs(anguloDesejado - servo.UltimoAnguloDesejado) > 0.01; + + if ((anguloDivergente || desejoMudou) && + PodeReenviarComando(servo.UltimoComandoEnviado, desejoMudou, agora)) + { + if (TentarEnviarComando( + () => GeneralJoystick.EnviarComandoSensoriamento(servo.Componente, servo.ID, anguloDesejado), + servo.ID)) + { + servo.UltimoComandoEnviado = agora; + servo.UltimoAnguloDesejado = anguloDesejado; + } + } } } + } - foreach (var rele in Reles.Where(x => x.Inicializado)) + private static bool TentarEnviarComando(Action enviar, string componenteId) + { + try { - Estado estadoDesejado = rele.Controlar && rele.StatusControle == Estado.Ligado ? Estado.Ligado : Estado.Desligado; - bool estadoDivergente = estadoDesejado != rele._EstadoLeitura; - if (estadoDivergente) - { - GeneralJoystick.EnviarComandoSensoriamento(rele.Componente, rele.ID, estadoDesejado); - } + enviar(); + return true; } + catch (Exception ex) + { + Variaveis.MostrarLog($"[SEN/TX] Falha ao comandar {componenteId}: {ex}"); + return false; + } + } - foreach (var servo in Servos.Where(x => x.Inicializado)) + private static bool PodeReenviarComando(DateTime ultimoEnvio, bool desejoMudou, DateTime agora) + { + return desejoMudou || + ultimoEnvio == DateTime.MinValue || + (agora - ultimoEnvio).TotalMilliseconds >= IntervaloReenvioComandoMs; + } + + private static SensorSinaleiroComportamentoModel CopiarComportamento(SensorSinaleiroComportamentoModel origem) + { + if (origem == null) + return null; + + return new SensorSinaleiroComportamentoModel() { - double anguloDesejado = servo.Controlar ? servo._AnguloControle : servo.AnguloNeutro; - bool anguloDivergente = Math.Abs(anguloDesejado - servo._AnguloLeiutra) > 0.01; - if (anguloDivergente) - { - GeneralJoystick.EnviarComandoSensoriamento(servo.Componente, servo.ID, anguloDesejado); - } - } + id = origem.id, + statusOperacao = origem.statusOperacao, + statusLED = origem.statusLED, + QtdPiscadas = origem.QtdPiscadas, + TempoMs = origem.TempoMs, + PausaMs = origem.PausaMs, + }; } private void AtualizaComponentesIniciados() { - DateTime Agora = DateTime.Now; - bool modConectado = DadosLeitura.UltimoComandoRespondido.AddMilliseconds(TempoLimiteConexao) > Agora; - if (!modConectado && DadosLeitura.Conectado) + lock (_estadoLock) { - DadosLeitura.Conectado = false; - } - foreach (var led in Sinaleiros.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) - { - led.Inicializado = false; - } - foreach (var rele in Reles.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) - { - rele.Inicializado = false; - } - foreach (var servo in Servos.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) - { - servo.Inicializado = false; - } - foreach (var sensor in Sensores.Where(x => x.Inicializado && x.UltimaLeitura.AddMilliseconds(TempoLimiteConexao) < Agora)) - { - sensor.Inicializado = false; + DateTime agora = DateTime.Now; + bool moduloComTelemetria = + DadosLeitura != null && + DadosLeitura.UltimoComandoRespondido != DateTime.MinValue && + DadosLeitura.UltimoComandoRespondido.AddMilliseconds(Math.Max(1, TempoLimiteConexao)) > agora; + + if (!moduloComTelemetria) + { + if (DadosLeitura != null) + DadosLeitura.Conectado = false; + + _ultimaRespostaDescoberta = DateTime.MinValue; + } + + foreach (var led in (Sinaleiros ?? new List()) + .Where(x => x != null && x.Inicializado && LeituraExpirada(x.UltimaLeitura, agora))) + led.Inicializado = false; + + foreach (var rele in (Reles ?? new List()) + .Where(x => x != null && x.Inicializado && LeituraExpirada(x.UltimaLeitura, agora))) + rele.Inicializado = false; + + foreach (var servo in (Servos ?? new List()) + .Where(x => x != null && x.Inicializado && LeituraExpirada(x.UltimaLeitura, agora))) + servo.Inicializado = false; + + foreach (var sensor in (Sensores ?? new List()) + .Where(x => x != null && x.Inicializado && LeituraExpirada(x.UltimaLeitura, agora))) + sensor.Inicializado = false; } } + private static bool LeituraExpirada(DateTime ultimaLeitura, DateTime agora) + { + return ultimaLeitura == DateTime.MinValue || + ultimaLeitura.AddMilliseconds(Math.Max(1, TempoLimiteConexao)) < agora; + } + public void AtualizarStatusSinaleiros(ModoOperacao Modo, StatusOperacao _statusOperacao) { - var ledMn = Sinaleiros.FirstOrDefault(x => x.ID == "LEDMN"); - var ledAt = Sinaleiros.FirstOrDefault(x => x.ID == "LEDAT"); - var ledOp = Sinaleiros.FirstOrDefault(x => x.ID == "LEDOP"); - if (ledMn != null) ledMn.Comportamento = VariaveisEquipamento.ComportamentoLedPorStatus(Modo == ModoOperacao.Manual ? StatusLED.Aceso : StatusLED.Apagado); - if (ledAt != null) ledAt.Comportamento = VariaveisEquipamento.ComportamentoLedPorStatus(Modo == ModoOperacao.Manual ? StatusLED.Apagado : StatusLED.Aceso); - if (ledOp != null) ledOp.Comportamento = VariaveisEquipamento.ComportamentoLedPorStatus(StatusLED.Piscando, status_operacao: _statusOperacao); + lock (_estadoLock) + { + var ledMn = Sinaleiros?.FirstOrDefault(x => x?.ID == "LEDMN"); + var ledAt = Sinaleiros?.FirstOrDefault(x => x?.ID == "LEDAT"); + var ledOp = Sinaleiros?.FirstOrDefault(x => x?.ID == "LEDOP"); + + if (ledMn != null) + ledMn.Comportamento = VariaveisEquipamento.ComportamentoLedPorStatus(Modo == ModoOperacao.Manual ? StatusLED.Aceso : StatusLED.Apagado); + + if (ledAt != null) + ledAt.Comportamento = VariaveisEquipamento.ComportamentoLedPorStatus(Modo == ModoOperacao.Manual ? StatusLED.Apagado : StatusLED.Aceso); + + if (ledOp != null) + ledOp.Comportamento = VariaveisEquipamento.ComportamentoLedPorStatus(StatusLED.Piscando, status_operacao: _statusOperacao); + } } } @@ -2418,7 +2837,10 @@ namespace AgroBase.Models.Modules { get { - return ValoresLeituras.OrderByDescending(x => x.atual.lidoEm).FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue; + return ValoresLeituras? + .Where(x => x?.atual != null) + .OrderByDescending(x => x.atual.lidoEm) + .FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue; } } public List Funcoes { get; set; } @@ -2433,22 +2855,27 @@ namespace AgroBase.Models.Modules (byte)(conectar ? 1 : 0), }; - var _pinout = Pinout.Clone(); - _pinout.Pinos = _pinout.Pinos.Where(x => x.ComponenteID == ID).ToList(); - if (_pinout.Pinos.Count > 0) + var _pinout = Pinout?.Clone(); + if (_pinout?.Pinos != null) { - bool enviaBarramento = Dispositivo == T_Code.Sen; + _pinout.Pinos = _pinout.Pinos.Where(x => x != null && x.ComponenteID == ID).ToList(); + if (_pinout.Pinos.Count > 0) + { + bool enviaBarramento = Dispositivo == T_Code.Sen; - byte[] pinos = PinoutModel.PinoProtocolo(_pinout, Funcoes, enviaBarramento); - config.AddRange(pinos); + byte[] pinos = PinoutModel.PinoProtocolo(_pinout, Funcoes ?? new List(), enviaBarramento); + if (pinos != null) + config.AddRange(pinos); + } } - foreach (var parametro in Parametros) + foreach (var parametro in Parametros ?? new List()) { - if (parametro.Enviar) + if (parametro?.Enviar == true) { byte[] paramBytes = parametro.InterpretarParametro(); - config.AddRange(paramBytes); + if (paramBytes != null) + config.AddRange(paramBytes); } } @@ -2465,6 +2892,7 @@ namespace AgroBase.Models.Modules public S_Code Componente { get; set; } public bool Controlar { get; set; } public bool Mandatorio { get; set; } + public bool Liberado => !Mandatorio || (Controlar && Inicializado); public List Funcoes { get; set; } public SensorSinaleiroComportamentoModel Comportamento { get; set; } public bool Inicializado { get; set; } @@ -2472,12 +2900,15 @@ namespace AgroBase.Models.Modules { get { - return ValoresLeituras.OrderByDescending(x => x.atual.lidoEm).FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue; + return ValoresLeituras? + .Where(x => x?.atual != null) + .OrderByDescending(x => x.atual.lidoEm) + .FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue; } } public List ValoresLeituras { get; set; } - public int _StatusLedId => (int)(ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.StatusLedID)?.atual?.valor ?? 99); - public StatusLED _StatusLed => (StatusLED)(ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.StatusLedLeitura)?.atual?.valor ?? StatusLED.Apagado); + public int _StatusLedId => (int)(ValoresLeituras?.FirstOrDefault(x => x?.funcao == FuncoesPinout.StatusLedID)?.atual?.valor ?? 99); + public StatusLED _StatusLed => (StatusLED)(ValoresLeituras?.FirstOrDefault(x => x?.funcao == FuncoesPinout.StatusLedLeitura)?.atual?.valor ?? StatusLED.Apagado); public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue; @@ -2485,7 +2916,7 @@ namespace AgroBase.Models.Modules public (CanMessagePosicaoDados, byte[]) ProtocoloConfiguracao(PinoutDataModel pinout, bool conectar) { - var pino = pinout.Pinos.FirstOrDefault(p => p.ComponenteID == ID); + var pino = pinout?.Pinos?.FirstOrDefault(p => p?.ComponenteID == ID); List config = new List { (byte)Componente, @@ -2499,13 +2930,19 @@ namespace AgroBase.Models.Modules public (int, CanMessagePosicaoDados, byte[]) ProtocoloComando() { + var comportamento = Comportamento ?? new SensorSinaleiroComportamentoModel() + { + id = 0, + statusLED = StatusLED.Apagado, + }; + List frame = new List { - (byte)Comportamento.id, - (byte)Comportamento.statusLED, - (byte)Comportamento.QtdPiscadas, - (byte)(Comportamento.TempoMs / 10), - (byte)(Comportamento.PausaMs / 10) + (byte)Math.Max(0, Math.Min(byte.MaxValue, comportamento.id)), + (byte)comportamento.statusLED, + (byte)Math.Max(0, Math.Min(byte.MaxValue, comportamento.QtdPiscadas)), + (byte)Math.Max(0, Math.Min(byte.MaxValue, comportamento.TempoMs / 10)), + (byte)Math.Max(0, Math.Min(byte.MaxValue, comportamento.PausaMs / 10)) }; return (ID_Num, CanMessagePosicaoDados.Command1, frame.ToArray()); @@ -2536,11 +2973,14 @@ namespace AgroBase.Models.Modules { get { - return ValoresLeituras.OrderByDescending(x => x.atual.lidoEm).FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue; + return ValoresLeituras? + .Where(x => x?.atual != null) + .OrderByDescending(x => x.atual.lidoEm) + .FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue; } } public List ValoresLeituras { get; set; } - public Estado _EstadoLeitura => ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.EstadoLeitura)?.atual?.valor ?? Estado.Desligado; + public Estado _EstadoLeitura => (Estado)(ValoresLeituras?.FirstOrDefault(x => x?.funcao == FuncoesPinout.EstadoLeitura)?.atual?.valor ?? 0); public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue; @@ -2549,7 +2989,7 @@ namespace AgroBase.Models.Modules public (CanMessagePosicaoDados, byte[]) ProtocoloConfiguracao(PinoutDataModel pinout, bool conectar) { - var pino = pinout.Pinos.FirstOrDefault(p => p.ComponenteID == ID); + var pino = pinout?.Pinos?.FirstOrDefault(p => p?.ComponenteID == ID); List config = new List { (byte)Componente, @@ -2596,13 +3036,16 @@ namespace AgroBase.Models.Modules } public int AnguloNeutro { get; set; } public int _AnguloControle { get; set; } - public int _AnguloLeiutra => (int)(ValoresLeituras?.FirstOrDefault(x => x.funcao == FuncoesPinout.ServoAnguloLeitura)?.atual?.valor ?? -1); + public int _AnguloLeiutra => (int)(ValoresLeituras?.FirstOrDefault(x => x?.funcao == FuncoesPinout.ServoAnguloLeitura)?.atual?.valor ?? -1); public bool Inicializado { get; set; } public DateTime UltimaLeitura { get { - return ValoresLeituras.OrderByDescending(x => x.atual.lidoEm).FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue; + return ValoresLeituras? + .Where(x => x?.atual != null) + .OrderByDescending(x => x.atual.lidoEm) + .FirstOrDefault()?.atual?.lidoEm ?? DateTime.MinValue; } } public List ValoresLeituras { get; set; } @@ -2612,7 +3055,7 @@ namespace AgroBase.Models.Modules public (CanMessagePosicaoDados, byte[]) ProtocoloConfiguracao(PinoutDataModel pinout, bool conectar) { - var pino = pinout.Pinos.FirstOrDefault(p => p.ComponenteID == ID); + var pino = pinout?.Pinos?.FirstOrDefault(p => p?.ComponenteID == ID); List config = new List { @@ -2628,12 +3071,14 @@ namespace AgroBase.Models.Modules public (int, CanMessagePosicaoDados, byte[]) ProtocoloComando() { + int anguloSeguro = Math.Max(0, Math.Min(180, _AnguloControle)); + return ( ID_Num, CanMessagePosicaoDados.Command1, new byte[] { - (byte)_AnguloControle + (byte)anguloSeguro } ); } @@ -2675,6 +3120,8 @@ namespace AgroBase.Models.Modules hash = hash * 23 + QtdPiscadas.GetHashCode(); hash = hash * 23 + TempoMs.GetHashCode(); hash = hash * 23 + PausaMs.GetHashCode(); + hash = hash * 23 + id.GetHashCode(); + hash = hash * 23 + statusOperacao.GetHashCode(); return hash; } } @@ -2711,25 +3158,64 @@ namespace AgroBase.Models.Modules public byte[] InterpretarParametro() { - string valor = Valor.Replace(",", ".").Trim(); + if (QtdBytes != 1 && QtdBytes != 2) + throw new InvalidOperationException($"Parâmetro '{Descricao}' possui QtdBytes inválido: {QtdBytes}."); + + string valor = (Valor ?? string.Empty).Replace(",", ".").Trim(); + if (valor.Length == 0) + throw new FormatException($"Parâmetro '{Descricao}' não possui valor."); // HEX direto if (valor.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { - byte parsedHex = Convert.ToByte(valor, 16); - return QtdBytes == 2 ? new byte[] { 0x00, parsedHex } : new byte[] { parsedHex }; + uint parsedHex; + if (!uint.TryParse(valor.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedHex)) + throw new FormatException($"Valor hexadecimal inválido em '{Descricao}': {Valor}."); + + uint maximo = QtdBytes == 1 ? byte.MaxValue : ushort.MaxValue; + if (parsedHex > maximo) + throw new OverflowException($"Valor de '{Descricao}' não cabe em {QtdBytes} byte(s): {Valor}."); + + if (QtdBytes == 1) + return new byte[] { (byte)parsedHex }; + + return new byte[] + { + (byte)(parsedHex >> 8), + (byte)(parsedHex & 0xFF), + }; } // FLOAT com ou sem ponto - float f = float.Parse(valor, CultureInfo.InvariantCulture); - short final = (short)(Escalar ? f * 100 : f); + double numero; + if (!double.TryParse(valor, NumberStyles.Float, CultureInfo.InvariantCulture, out numero)) + throw new FormatException($"Valor numérico inválido em '{Descricao}': {Valor}."); + + double escalado = Escalar ? numero * 100.0 : numero; + if (double.IsNaN(escalado) || double.IsInfinity(escalado)) + throw new OverflowException($"Valor não finito em '{Descricao}': {Valor}."); + + if (escalado < int.MinValue || escalado > int.MaxValue) + throw new OverflowException($"Valor fora do intervalo inteiro em '{Descricao}': {Valor}."); + + int final = (int)escalado; if (QtdBytes == 1) - return new byte[] { (byte)(final & 0xFF) }; + { + if (final < sbyte.MinValue || final > byte.MaxValue) + throw new OverflowException($"Valor de '{Descricao}' não cabe em 1 byte: {final}."); + + return new byte[] { unchecked((byte)final) }; + } + + if (final < short.MinValue || final > ushort.MaxValue) + throw new OverflowException($"Valor de '{Descricao}' não cabe em 2 bytes: {final}."); + + ushort codificado = unchecked((ushort)final); return new byte[] { - (byte)(final >> 8), - (byte)(final & 0xFF) + (byte)(codificado >> 8), + (byte)(codificado & 0xFF) }; } } @@ -2755,18 +3241,165 @@ namespace AgroBase.Models.Modules public T_Code Dispositivo { get; set; } public int Versao { get; set; } public string Mod_ID { get; set; } - public int QualidadeWifi { get; set; } public int LatenciaLoop { get; set; } + public Dictionary StatusI2C { get; set; } public DateTime UltimoComandoRespondido { get; set; } = DateTime.MinValue; - public List Sensores { get; set; } - public List Sinaleiros { get; set; } - public List Reles { get; set; } - public List Servos { get; set; } } public class SenHandler : ICanMessageHandler { - public T_Code Dispositivo { get; set; } = Variaveis.OperacaoEmAndamento?.DispSen?.Dados?.Dispositivo ?? T_Code.Sen; + public T_Code Dispositivo { get; set; } = T_Code.Sen; + private readonly object _logFrameLock = new object(); + private DateTime _ultimoLogFrameInvalido = DateTime.MinValue; + private int _framesInvalidosSuprimidos = 0; + + private void LogFrameInvalido(string motivo) + { + lock (_logFrameLock) + { + DateTime agora = DateTime.Now; + if ((agora - _ultimoLogFrameInvalido).TotalMilliseconds < 1000) + { + _framesInvalidosSuprimidos++; + return; + } + + string suprimidos = _framesInvalidosSuprimidos > 0 + ? $"; suprimidos={_framesInvalidosSuprimidos}" + : string.Empty; + + Variaveis.MostrarLog($"[SEN/RX] Frame descartado: {motivo}{suprimidos}"); + _ultimoLogFrameInvalido = agora; + _framesInvalidosSuprimidos = 0; + } + } + + private bool FrameValido(byte[] data, S_Code componente, CanMessagePosicaoDados posicao, out string motivo) + { + motivo = null; + + if (data == null || data.Length < 2) + { + motivo = "payload nulo ou menor que 2 bytes"; + return false; + } + + int minimo; + + switch (componente) + { + case S_Code.sMOD: + minimo = + posicao == CanMessagePosicaoDados.Status ? 5 : + posicao == CanMessagePosicaoDados.Dados1 ? 8 : + posicao == CanMessagePosicaoDados.Dados2 ? 8 : + posicao == CanMessagePosicaoDados.Dados3 ? 8 : + posicao == CanMessagePosicaoDados.Dados4 ? 8 : + -1; + break; + case S_Code.sTOD: + minimo = posicao == CanMessagePosicaoDados.DadosAll ? 4 : -1; + break; + case S_Code.sNTC: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 8 : -1; + break; + case S_Code.sCOR: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 8 : + posicao == CanMessagePosicaoDados.Dados2 ? 8 : -1; + break; + case S_Code.sLED: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 4 : -1; + break; + case S_Code.sRLE: + case S_Code.sSRV: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 3 : -1; + break; + case S_Code.sIMU: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 8 : + posicao == CanMessagePosicaoDados.Dados2 ? 8 : + posicao == CanMessagePosicaoDados.Dados3 ? 8 : + posicao == CanMessagePosicaoDados.Dados4 ? 8 : + posicao == CanMessagePosicaoDados.Dados5 ? 8 : -1; + break; + case S_Code.sGAS: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 8 : -1; + break; + case S_Code.sSHT: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 6 : -1; + break; + case S_Code.sLUZ: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 6 : -1; + break; + case S_Code.sIUV: + case S_Code.sCHV: + case S_Code.sMAG: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 6 : -1; + break; + case S_Code.sQAR: + minimo = posicao == CanMessagePosicaoDados.Status ? 3 : + posicao == CanMessagePosicaoDados.Dados1 ? 7 : + posicao == CanMessagePosicaoDados.Dados2 ? 6 : -1; + break; + default: + motivo = $"componente não suportado ({componente})"; + return false; + } + + if (minimo < 0) + { + motivo = $"posição {posicao} não suportada para {componente}"; + return false; + } + + if (data.Length < minimo) + { + motivo = $"payload curto para {componente}/{posicao}: {data.Length} < {minimo}"; + return false; + } + + if (componente == S_Code.sMOD && posicao == CanMessagePosicaoDados.Status && data[3] > 1) + { + motivo = $"flag Conectado inválida no sMOD: {data[3]}"; + return false; + } + + if (componente != S_Code.sMOD && posicao == CanMessagePosicaoDados.Status && data[2] > 1) + { + motivo = $"flag Iniciado inválida em {componente}: {data[2]}"; + return false; + } + + if (componente == S_Code.sLED && posicao == CanMessagePosicaoDados.Dados1 && + !Enum.IsDefined(typeof(StatusLED), (StatusLED)data[3])) + { + motivo = $"status de LED inválido: {data[3]}"; + return false; + } + + if (componente == S_Code.sRLE && posicao == CanMessagePosicaoDados.Dados1 && + !Enum.IsDefined(typeof(Estado), (Estado)data[2])) + { + motivo = $"estado de relé inválido: {data[2]}"; + return false; + } + + if (componente == S_Code.sSRV && posicao == CanMessagePosicaoDados.Dados1 && data[2] > 180) + { + motivo = $"ângulo de servo inválido: {data[2]}"; + return false; + } + + return true; + } private double ConverterByteParaDouble(byte[] data, int idx) { @@ -2828,577 +3461,865 @@ namespace AgroBase.Models.Modules return milivolts / 1000.0; } - private S_Code DetectarComponenteDoID_Num(int ID_Num) + + #region DIAGNOSTICO + private static bool Bit(byte valor, int bit) { - if (ID_Num == Variaveis.ID_Num_sMOD) return S_Code.sMOD; - if (ID_Num == Variaveis.ID_Num_sTOD) return S_Code.sTOD; - if (ID_Num == Variaveis.ID_Num_sLRA) return S_Code.sLRA; - - var Disp = Variaveis.OperacaoEmAndamento.DispSen.Dados; - var sensor = Disp.Sensores.FirstOrDefault(x => x.ID_Num == ID_Num); - var rele = Disp.Reles.FirstOrDefault(x => x.ID_Num == ID_Num); - var sinaleiro = Disp.Sinaleiros.FirstOrDefault(x => x.ID_Num == ID_Num); - var servo = Disp.Servos.FirstOrDefault(x => x.ID_Num == ID_Num); - - return sensor?.Componente ?? rele?.Componente ?? sinaleiro?.Componente ?? servo?.Componente ?? S_Code.sVZO; + return (valor & (1 << bit)) != 0; } - private void AtualizarDados(int CompID_Num, CanMessagePosicaoDados Posicao, S_Code componente, List Parametros) + private void LogDiagnosticoI2CDados1(byte[] data) { - var Dados = Variaveis.OperacaoEmAndamento.DispSen.Dados; + byte estado = data[2]; + byte tca = data[3]; - var sensor = Dados.Sensores.FirstOrDefault(x => x.ID_Num == CompID_Num); - var rele = Dados.Reles.FirstOrDefault(x => x.ID_Num == CompID_Num); - var sinaleiro = Dados.Sinaleiros.FirstOrDefault(x => x.ID_Num == CompID_Num); - var servo = Dados.Servos.FirstOrDefault(x => x.ID_Num == CompID_Num); + bool i2c = + Bit(estado, 0); - List valoresLeituras = new List(); + bool mux = + Bit(estado, 1); + + bool muxOk = + Bit(estado, 2); + + bool recovery = + Bit(estado, 3); + + bool pendente = + Bit(estado, 4); + + bool sda = + Bit(estado, 5); + + bool scl = + Bit(estado, 6); + + bool possuiFalha = + Bit(estado, 7); + + + bool rdyConfigurado = + Bit(tca, 0); + + bool rdy = + Bit(tca, 1); + + bool enConfigurado = + Bit(tca, 2); + + bool en = + Bit(tca, 3); + + bool quarentenaMux = + Bit(tca, 4); + + bool quarentenaAds = + Bit(tca, 5); + + + int geracao = + data[4]; + + int recoveries = + data[5]; + + int falhasRecovery = + data[6]; + + int falhasTx = + data[7]; + + + string tcaRdy = + rdyConfigurado + ? (rdy ? "HIGH" : "LOW") + : "NA"; + + string tcaEn = + enConfigurado + ? (en ? "HIGH" : "LOW") + : "NA"; + + + Variaveis.MostrarLog( + "[SEN/I2C] BASE" + + $" | wire={(i2c ? 1 : 0)}" + + $" mux={(mux ? 1 : 0)}" + + $" mux_ok={(muxOk ? 1 : 0)}" + + $" recovery={(recovery ? 1 : 0)}" + + $" pendente={(pendente ? 1 : 0)}" + + $" SDA={(sda ? 1 : 0)}" + + $" SCL={(scl ? 1 : 0)}" + + $" TCA_RDY={tcaRdy}" + + $" TCA_EN={tcaEn}" + + $" geracao={geracao}" + + $" recoveries={recoveries}" + + $" falhas_recovery={falhasRecovery}" + + $" falhas_tx={falhasTx}" + + $" q_mux={(quarentenaMux ? 1 : 0)}" + + $" q_ads={(quarentenaAds ? 1 : 0)}" + + $" historico_falha={(possuiFalha ? 1 : 0)}" + ); + } + + private void LogDiagnosticoI2CDados2(byte[] data) + { + byte idFalha = data[2]; + byte canalFalha = data[3]; + + byte muxEsperado = data[4]; + byte muxValidado = data[5]; + byte muxQuarentena = data[6]; + byte inasIniciados = data[7]; + + + string idFalhaTxt = + idFalha == 0xFF + ? "nenhum" + : NomeIdI2CDebug(idFalha); + + string canalFalhaTxt = + canalFalha == 0xFF + ? "nenhum" + : canalFalha.ToString(); + + + Variaveis.MostrarLog( + "[SEN/I2C] TOPO" + + $" | ultimo_id={idFalhaTxt}" + + $" ultimo_canal={canalFalhaTxt}" + + $" mux_esperado=0x{muxEsperado:X2}" + + $" mux_validado=0x{muxValidado:X2}" + + $" q_mux=0x{muxQuarentena:X2}" + + $" q_mux_sensores={DecodificarQuarentenaMux(muxQuarentena)}" + + $" ina_mask=0x{inasIniciados:X2}" + + $" inas={DecodificarInaMask(inasIniciados)}" + ); + } + + private void LogDiagnosticoI2CDados3(byte[] data) + { + if (data == null || data.Length < 8) + return; + + ushort maxAdsMs = Converter2BytesU16(data, 2); + ushort timeoutsAds = Converter2BytesU16(data, 4); + ushort readyDropsAds = Converter2BytesU16(data, 6); + + Console.WriteLine( + "[AGROBASE] [SEN/I2C] ADS | " + + $"max_ms={maxAdsMs} " + + $"timeouts={timeoutsAds} " + + $"ready_drops={readyDropsAds}" + ); + + } + + private void LogDiagnosticoI2CDados4(byte[] data) + { + if (data == null || data.Length < 8) + return; + + ushort maxStart = Converter2BytesU16(data, 2); + ushort maxRead = Converter2BytesU16(data, 4); + ushort falhasIo = Converter2BytesU16(data, 6); + + Console.WriteLine( + "[AGROBASE] [SEN/I2C] ADS_IO | " + + $"start_max={maxStart}ms " + + $"read_max={maxRead}ms " + + $"io_fail={falhasIo}" + ); + } + + private static string NomeIdI2CDebug(byte id) + { + switch (id) + { + case 11: return "AB3V(11)"; + case 12: return "AB5V(12)"; + case 13: return "AB7V2(13)"; + case 14: return "AB12V(14)"; + case 15: return "AB19V(15)"; + case 16: return "AB24V(16)"; + case 17: return "AB36V(17)"; + + case 200: + return "MUX(200)"; + + case 201: + return "MCP(201)"; + + default: + return id.ToString(); + } + } + + private static string DecodificarInaMask(byte mask) + { + string[] nomes = + { + "AB3V", + "AB5V", + "AB7V2", + "AB12V", + "AB19V", + "AB24V", + "AB36V" + }; + + var ativos = + new List(); + + for (int i = 0; i < nomes.Length; i++) + { + if ((mask & (1 << i)) != 0) + ativos.Add(nomes[i]); + } + + return ativos.Count > 0 + ? string.Join(",", ativos) + : "nenhum"; + } + + private static string DecodificarQuarentenaMux(byte mask) + { + var itens = new List(); + + for (int canal = 0; canal < 8; canal++) + { + if ((mask & (1 << canal)) == 0) + continue; + + string nome; + + switch (canal) + { + case 1: + nome = "CH1/AB19V"; + break; + + case 2: + nome = "CH2/AB7V2"; + break; + + case 3: + nome = "CH3/AB12V"; + break; + + case 4: + nome = "CH4/AB5V"; + break; + + case 5: + nome = "CH5/AB3V"; + break; + + case 6: + nome = "CH6/AB24V"; + break; + + case 7: + nome = "CH7/AB36V"; + break; + + default: + nome = $"CH{canal}"; + break; + } + + itens.Add(nome); + } + + return itens.Count > 0 + ? string.Join(",", itens) + : "nenhum"; + } + + #endregion + + + private bool TryDetectarComponenteDoID_Num(SensoriamentoModel dados, int ID_Num, out S_Code componente, out string motivo) + { + componente = default(S_Code); + motivo = null; + + if (ID_Num == Variaveis.ID_Num_sMOD) + { + componente = S_Code.sMOD; + return true; + } + + if (ID_Num == Variaveis.ID_Num_sTOD) + { + componente = S_Code.sTOD; + return true; + } + + if (dados == null) + { + motivo = "modelo do SEN indisponível"; + return false; + } + + var encontrados = new List(); + + encontrados.AddRange((dados.Sensores ?? new List()) + .Where(x => x != null && x.ID_Num == ID_Num) + .Select(x => x.Componente)); + encontrados.AddRange((dados.Reles ?? new List()) + .Where(x => x != null && x.ID_Num == ID_Num) + .Select(x => x.Componente)); + encontrados.AddRange((dados.Sinaleiros ?? new List()) + .Where(x => x != null && x.ID_Num == ID_Num) + .Select(x => x.Componente)); + encontrados.AddRange((dados.Servos ?? new List()) + .Where(x => x != null && x.ID_Num == ID_Num) + .Select(x => x.Componente)); + + if (encontrados.Count == 0) + { + motivo = $"ID_Num desconhecido ({ID_Num})"; + return false; + } + + if (encontrados.Count > 1) + { + motivo = $"ID_Num ambíguo ({ID_Num}); ocorrências={encontrados.Count}"; + return false; + } + + componente = encontrados[0]; + return true; + } + + private void AtualizarDados(SensoriamentoModel Dados, int CompID_Num, CanMessagePosicaoDados Posicao, S_Code componente, List Parametros) + { + if (Dados == null) + throw new InvalidOperationException("Modelo do SEN indisponível durante atualização."); + + var sensor = Dados.Sensores?.FirstOrDefault(x => x?.ID_Num == CompID_Num); + var rele = Dados.Reles?.FirstOrDefault(x => x?.ID_Num == CompID_Num); + var sinaleiro = Dados.Sinaleiros?.FirstOrDefault(x => x?.ID_Num == CompID_Num); + var servo = Dados.Servos?.FirstOrDefault(x => x?.ID_Num == CompID_Num); + + List valoresLeituras = null; if (sensor != null) valoresLeituras = sensor.ValoresLeituras; else if (rele != null) valoresLeituras = rele.ValoresLeituras; else if (sinaleiro != null) valoresLeituras = sinaleiro.ValoresLeituras; else if (servo != null) valoresLeituras = servo.ValoresLeituras; - for (int i = 0; i < valoresLeituras.Where(x => x.posicao == Posicao).Count(); i++) + var leiturasDaPosicao = valoresLeituras? + .Where(x => x != null && x.posicao == Posicao) + .OrderBy(x => x.indice) + .ToList(); + + if (leiturasDaPosicao == null || leiturasDaPosicao.Count == 0) + throw new InvalidOperationException($"Sem mapeamento de leitura para {componente}/{CompID_Num}/{Posicao}."); + + if (Parametros == null || Parametros.Count < leiturasDaPosicao.Count) + throw new InvalidOperationException( + $"Parâmetros insuficientes para {componente}/{CompID_Num}/{Posicao}: " + + $"recebidos={Parametros?.Count ?? 0}, esperados={leiturasDaPosicao.Count}." + ); + + foreach (var valor in leiturasDaPosicao) { - try - { - var valor = valoresLeituras.FirstOrDefault(x => x.posicao == Posicao && x.indice == i); - if (valor != null) - { - var v = Parametros.Count > i ? (Parametros[i]) : -1; - if (v is sbyte || v is byte || v is short || v is ushort || v is int || v is uint || v is long || v is ulong || v is float || v is double || v is decimal) - { - v += valor.offset; - valor.atual.valor = v; - } - else - { - valor.atual.valor = v; - } + int indice = valor.indice; + if (indice < 0 || indice >= Parametros.Count) + throw new InvalidOperationException($"Índice de leitura inválido em {componente}/{CompID_Num}/{Posicao}: {indice}."); - if (valor.funcao == FuncoesPinout.Iniciado && valor.posicao == CanMessagePosicaoDados.Status) - { - if (sensor != null) - sensor.Inicializado = v; - if (rele != null) - rele.Inicializado = v; - if (sinaleiro != null) - sinaleiro.Inicializado = v; - if (servo != null) - servo.Inicializado = v; - } - } + if (valor.atual == null) + valor.atual = new CanCommandFreqModel(); + + dynamic v = Parametros[indice]; + if (v is sbyte || v is byte || v is short || v is ushort || v is int || v is uint || + v is long || v is ulong || v is float || v is double || v is decimal) + { + v += valor.offset; + valor.atual.valor = v; } - catch (Exception ex) + else { + valor.atual.valor = v; + } + if (valor.funcao == FuncoesPinout.Iniciado && valor.posicao == CanMessagePosicaoDados.Status) + { + if (!(v is bool)) + throw new InvalidOperationException($"Status Iniciado não booleano em {componente}/{CompID_Num}: {v}."); + + bool iniciado = (bool)v; + if (sensor != null) sensor.Inicializado = iniciado; + if (rele != null) rele.Inicializado = iniciado; + if (sinaleiro != null) sinaleiro.Inicializado = iniciado; + if (servo != null) servo.Inicializado = iniciado; } } } public void ProcessarMensagem(CanMessage mensagem) { - var op = Variaveis.OperacaoEmAndamento; + if (mensagem?.DataRx == null || mensagem.DataRx.Length < 2) + { + LogFrameInvalido("payload nulo ou incompleto"); + return; + } CanMessagePosicaoDados posicao = (CanMessagePosicaoDados)mensagem.DataRx[0]; byte ID_Num = mensagem.DataRx[1]; var data = mensagem.DataRx; - S_Code componente = DetectarComponenteDoID_Num(ID_Num); + var op = Variaveis.OperacaoEmAndamento; + var Dados = op?.DispSen?.Dados; - var Dados = op.DispSen.Dados; - - //Console.WriteLine($"[SEN] Componente={componente.ToString()}, ID_Num={ID_Num}, Posicao={posicao.ToString()}, Data={FuncoesGlobais.ConverterComandoBytesParaTexto(data)}"); - - switch (componente) + if (Dados == null) { - case S_Code.sMOD: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - T_Code tipoModulo = (T_Code)data[2]; - bool conectado = data[3] == 1; - int versao = data[4]; - - //Console.WriteLine($"[SEN] Resposta {ID_Num} recebida. Tipo={tipoModulo}, Conectado={conectado}, Versão={versao}"); - - Dados.DadosLeitura.Conectado = conectado; - Dados.DadosLeitura.Dispositivo = tipoModulo; - Dados.DadosLeitura.Versao = versao; - break; - } - } - break; - } - //case S_Code.sLRA: - // { - // var loraService = Dados.loRaService; - // switch (posicao) - // { - // case CanMessagePosicaoDados.Status: - // { - // bool Conectado = data[2] == 1; - // bool Configurado = data[3] == 1; - // //Console.WriteLine($"[LRA] Resposta {ID_Num} recebida. Conectado={Conectado}, Configurado={Configurado}"); - // loraService.Conectado = Conectado; - // loraService.Configurado = Configurado; - // break; - // } - // case CanMessagePosicaoDados.Dados1: - // { - // byte address = data[2]; - // byte baudAndAirRate = data[3]; - // byte baud = (byte)(baudAndAirRate & 0b11100000); - // byte airRate = (byte)((baudAndAirRate & 0b00011100) >> 2); - // byte packetSizeAndPower = data[4]; - // byte packetSize = (byte)((packetSizeAndPower & 0b11000000)); - // byte power = (byte)(packetSizeAndPower & 0b00000011); - // byte channel = data[5]; - // byte tranModeAndWorCycle = data[6]; - // byte tranMode = (byte)(tranModeAndWorCycle & 0b01000000); - // byte worCycle = (byte)(tranModeAndWorCycle & 0b00000111); - // //Console.WriteLine($"[LRA] Resposta {ID_Num} recebida. Address={address.ToString("X")}, BaudRate={baud.ToString("X")}, AirRate={airRate.ToString("X")}, PacketSize={packetSize.ToString("X")}, Power={power.ToString("X")}, Channel={channel.ToString("X")}, transMode={tranMode.ToString("X")}, WorCycle={worCycle.ToString("X")}"); - // loraService.ParametrosGet.address = address; - // loraService.ParametrosGet.baudRate = baud; - // loraService.ParametrosGet.airRate = airRate; - // loraService.ParametrosGet.packetSize = packetSize; - // loraService.ParametrosGet.power = power; - // loraService.ParametrosGet.channel = channel; - // loraService.ParametrosGet.worCycle = worCycle; - // loraService.ParametrosGet.tranMode = tranMode; - // loraService.ParametrosGet.UltimaLeitura = DateTime.Now; - // break; - // } - // default: - // { - // DadosLoRaParse tipoDado = (DadosLoRaParse)posicao; - // //Console.WriteLine($"[LRA] Dados recebidos ({tipoDado.ToString()}). " + string.Join(" ", data)); - // switch (tipoDado) - // { - // case DadosLoRaParse.ComandoControle: - // { - // BotoesJoystick tecla = (BotoesJoystick)data[2]; - // int percentualVelocidadeSp = data[3]; - // TipoMovimentoDirecional tipoMovimentoDirecional = (TipoMovimentoDirecional)data[4]; - // int anguloMp = data[5]; - // //int velocidadeMp = data[6]; - // T_Code dispositivo = (T_Code)data[6]; - // bool emergencia = data[7] == 1; - // - // OperacaoComandoBaseModel Controle = new OperacaoComandoBaseModel() - // { - // Momento = DateTime.Now, - // Tecla = tecla, - // Controle = new OperacaoComandoBaseControleModel() - // { - // AnguloSP = anguloMp, - // PercentualVelocidadeSP = percentualVelocidadeSp, - // TipoMovimentoDirecional = tipoMovimentoDirecional, - // }, - // Emergencia = emergencia, - // Dispositivo = dispositivo - // }; - // op.ExecutaComandoDaBase(Controle); - // break; - // } - // case DadosLoRaParse.DadosLivre: - // { - // Console.WriteLine("[LRA] Dados livres LORA recebidos da base: " + FuncoesGlobais.ConverterComandoBytesParaTexto(data)); - // Variaveis.LoraService.MensagensRecebidasAvulsas.Add(new LoRaMensagemModel() - // { - // Lido = false, - // Momento = DateTime.Now, - // channel = Variaveis.LoraBaseParametros.channel, - // Remetente = Variaveis.LoraBaseParametros.address, - // Destinatario = Variaveis.LoraService.ParametrosGet.address, - // Data = data, - // Mensagem = string.Join("", data.Select(x => x.ToString("X"))) - // }); - // break; - // } - // case DadosLoRaParse.GpsQualidade: - // { - // bool iniciado = data[2] == 1; - // TiposCorrecaoGPS qualidadeFix = (TiposCorrecaoGPS)data[3]; - // int numeroSatelites = data[4]; - // double precisao = LoRaSerializer.DescompactarDecimal(data, 5); - // - // VariaveisOperacao.PosicaoBase.Inicializado = iniciado; - // VariaveisOperacao.PosicaoBase.QualidadeFix = qualidadeFix; - // VariaveisOperacao.PosicaoBase.NumeroSatelites = numeroSatelites; - // VariaveisOperacao.PosicaoBase.PrecisaoHorizontal = precisao; - // VariaveisOperacao.PosicaoBase.Momento = DateTime.Now; - // break; - // } - // case DadosLoRaParse.GpsLatitude: - // { - // double _lat = LoRaSerializer.DescompactarCoordenada(data.Skip(2).ToArray()); - // VariaveisOperacao.PosicaoBase.Latitude = _lat; - // VariaveisOperacao.PosicaoBase.Momento = DateTime.Now; - // break; - // } - // case DadosLoRaParse.GpsLongitude: - // { - // double _long = LoRaSerializer.DescompactarCoordenada(data.Skip(2).ToArray()); - // VariaveisOperacao.PosicaoBase.Longitude = _long; - // VariaveisOperacao.PosicaoBase.Momento = DateTime.Now; - // break; - // } - // case DadosLoRaParse.GpsGerais: - // { - // double _altitude = LoRaSerializer.DescompactarDecimal(data, 2); - // double _velocidade = LoRaSerializer.DescompactarDecimal(data, 4); - // double _orientacao = LoRaSerializer.DescompactarDecimal(data, 6); - // VariaveisOperacao.PosicaoBase.Altitude = _altitude; - // VariaveisOperacao.PosicaoBase.Velocidade = _velocidade; - // VariaveisOperacao.PosicaoBase.OrientacaoReal = _orientacao; - // VariaveisOperacao.PosicaoBase.Momento = DateTime.Now; - // break; - // } - // } - // Variaveis.LoraService.UltimoRxDados = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency; - // Variaveis.LoraService._ultimoRxDados = DateTime.UtcNow; - // break; - // } - // } - // break; - // } - case S_Code.sTOD: - { - switch (posicao) - { - case CanMessagePosicaoDados.DadosAll: - { - double latenciaLoop = ConverterByteParaDouble(data, 2) * 100; - Dados.DadosLeitura.LatenciaLoop = Convert.ToInt32(latenciaLoop); - //Console.WriteLine($"[SEN] Ciclo concluido. Latencia de loop: {latenciaLoop} ms"); - break; - } - } - break; - } - case S_Code.sNTC: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - ushort raw = Converter2BytesU16(data, 2); - double temperatura = ConverterTemperaturaNtc(data, 4); - double tensao = ConverterTensaoNtc(data, 6); - AtualizarDados(ID_Num, posicao, componente, new List() { raw, temperatura, tensao }); - //Variaveis.MostrarLog($"[SEN] sNTC | ID_Num: {ID_Num} | Posicao: {posicao} | Leitura: {raw} | Temperatura: {temperatura} | Tensao: {tensao}"); - break; - } - } - break; - } - case S_Code.sCOR: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - double busVoltage = ConverterByteParaDouble(data, 2); // continua igual - double current = ConverterByteParaDouble(data, 4) * 1000.0; // volts / 100, amps * 10 (volta para mA) - double power = ConverterByteParaDouble(data, 6) * 1000.0; // volta para mW - //Console.WriteLine($"[AB36V] Tensao {busVoltage}, Corrente: {current}, Power: {power}"); - AtualizarDados(ID_Num, posicao, componente, new List() { busVoltage, current, power }); - break; - } - case CanMessagePosicaoDados.Dados2: - { - double energia = ConverterByteParaDouble(data, 2) * 1000.0; // volta para mWh - double temperatura = ConverterByteParaDouble(data, 4); // volta para °C - double codAlarme = ConverterByteParaDouble(data, 6) * 100; // volta para °C - AtualizarDados(ID_Num, posicao, componente, new List() { energia, temperatura, codAlarme }); - break; - } - } - break; - } - case S_Code.sLED: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - int statusId = data[2]; - StatusLED statusLeitura = (StatusLED)data[3]; - AtualizarDados(ID_Num, posicao, componente, new List() { statusId, statusLeitura }); - break; - } - } - break; - } - case S_Code.sRLE: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - Estado estado = (Estado)data[2]; - AtualizarDados(ID_Num, posicao, componente, new List() { estado }); - break; - } - } - break; - } - case S_Code.sSRV: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - int angulo = data[2]; - AtualizarDados(ID_Num, posicao, componente, new List() { angulo }); - break; - } - } - break; - } - case S_Code.sIMU: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - double roll = ConverterByteParaDouble(data, 2); - double pitch = ConverterByteParaDouble(data, 4); - double yaw = ConverterByteParaDouble(data, 6); - AtualizarDados(ID_Num, posicao, componente, new List() { roll, pitch, yaw }); - break; - } - case CanMessagePosicaoDados.Dados2: - { - double temperatura = ConverterByteParaDouble(data, 2); - double pressao = ConverterValorEscala(data, 4, 10); - double altitude = ConverterValorEscala(data, 6, 10); - AtualizarDados(ID_Num, posicao, componente, new List() { temperatura, pressao, altitude }); - break; - } - case CanMessagePosicaoDados.Dados3: - { - double accX = ConverterByteParaDouble(data, 2); - double accY = ConverterByteParaDouble(data, 4); - double accZ = ConverterByteParaDouble(data, 6); - AtualizarDados(ID_Num, posicao, componente, new List() { accX, accY, accZ }); - break; - } - case CanMessagePosicaoDados.Dados4: - { - double gyrX = ConverterByteParaDouble(data, 2); - double gyrY = ConverterByteParaDouble(data, 4); - double gyrZ = ConverterByteParaDouble(data, 6); - AtualizarDados(ID_Num, posicao, componente, new List() { gyrX, gyrY, gyrZ }); - break; - } - case CanMessagePosicaoDados.Dados5: - { - double magX = ConverterByteParaDouble(data, 2); - double magY = ConverterByteParaDouble(data, 4); - double magZ = ConverterByteParaDouble(data, 6); - AtualizarDados(ID_Num, posicao, componente, new List() { magX, magY, magZ }); - break; - } - } - break; - } - case S_Code.sGAS: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - ushort raw = Converter2BytesU16(data, 2); - double ppm = ConverterBytesParaDouble32(data, 4); - AtualizarDados(ID_Num, posicao, componente, new List() { raw, ppm }); - break; - } - } - break; - } - case S_Code.sSHT: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - double temperatura = ConverterByteParaDouble(data, 2); - double umidade = ConverterByteParaDouble(data, 4); - AtualizarDados(ID_Num, posicao, componente, new List() { temperatura, umidade }); - break; - } - } - break; - } - case S_Code.sLUZ: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - double lux = ConverterBytesParaDouble32(data, 2); - AtualizarDados(ID_Num, posicao, componente, new List() { lux }); - break; - } - } - break; - } - case S_Code.sIUV: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - ushort raw = Converter2BytesU16(data, 2); - double iuv = ConverterByteParaDouble(data, 4); - AtualizarDados(ID_Num, posicao, componente, new List() { raw, iuv }); - break; - } - } - break; - } - case S_Code.sQAR: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - double tvoc = ConverterByteParaDouble(data, 2); - double eco2 = ConverterByteParaDouble(data, 4); - int aqi = data[6]; - AtualizarDados(ID_Num, posicao, componente, new List() { tvoc, eco2, aqi }); - break; - } - case CanMessagePosicaoDados.Dados2: - { - double temperatura = ConverterByteParaDouble(data, 2); - double umidade = ConverterByteParaDouble(data, 4); - AtualizarDados(ID_Num, posicao, componente, new List() { temperatura, umidade }); - break; - } - } - break; - } - case S_Code.sCHV: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - ushort raw = Converter2BytesU16(data, 2); - double percentual = ConverterByteParaDouble(data, 4); - AtualizarDados(ID_Num, posicao, componente, new List() { raw, percentual }); - break; - } - } - break; - } - case S_Code.sMAG: - { - switch (posicao) - { - case CanMessagePosicaoDados.Status: - { - bool Iniciado = data[2] == 1; - AtualizarDados(ID_Num, posicao, componente, new List() { Iniciado }); - break; - } - case CanMessagePosicaoDados.Dados1: - { - ushort raw = Converter2BytesU16(data, 2); - double angle = ConverterAngle100(data, 4); - AtualizarDados(ID_Num, posicao, componente, new List() { raw, angle }); - //Console.WriteLine($"[sMAG] {ID_Num}, Raw: {raw}, Angulo: {angle}"); - break; - } - } - break; - } - - + LogFrameInvalido("operação ou modelo do SEN indisponível"); + return; } - Dados.DadosLeitura.UltimoComandoRespondido = DateTime.Now; - //Console.WriteLine("Ultimo comando respondido: " + Dados.DadosLeitura.UltimoComandoRespondido.ToString("HH:mm:ss.fff") + ", Conectado: " + Dados.Conectado); + S_Code componente; + string motivoDeteccao; - op.DispMvd.Dados.AtualizarDadosSensoresExternos(componente, ID_Num); + lock (Dados.EstadoSyncRoot) + { + if (!TryDetectarComponenteDoID_Num(Dados, ID_Num, out componente, out motivoDeteccao)) + { + LogFrameInvalido(motivoDeteccao); + return; + } + + string motivoFrame; + if (!FrameValido(data, componente, posicao, out motivoFrame)) + { + LogFrameInvalido($"ID={ID_Num}; {motivoFrame}"); + return; + } + + Variaveis.MostrarLog($"[SEN] Componente={componente.ToString()}, ID_Num={ID_Num}, Data={FuncoesGlobais.ConverterComandoBytesParaTexto(data)}"); + + try + { + if (Dados.DadosLeitura == null) + Dados.DadosLeitura = new SensoriamentoMensagemJsonModel(); + + switch (componente) + { + case S_Code.sMOD: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + T_Code tipoModulo = (T_Code)data[2]; + + if (tipoModulo != T_Code.Sen) + { + LogFrameInvalido($"sMOD destinado a {tipoModulo}, esperado={T_Code.Sen}"); + return; + } + + bool conectado = data[3] == 1; + int versao = data[4]; + Dados.DadosLeitura.Conectado = conectado; + Dados.DadosLeitura.Dispositivo = tipoModulo; + Dados.DadosLeitura.Versao = versao; + Dados.DadosLeitura.Mod_ID = Dados.Modulo_ID; + break; + } + + case CanMessagePosicaoDados.Dados1: + { + LogDiagnosticoI2CDados1(data); + break; + } + + case CanMessagePosicaoDados.Dados2: + { + LogDiagnosticoI2CDados2(data); + break; + } + case CanMessagePosicaoDados.Dados3: + { + LogDiagnosticoI2CDados3(data); + break; + } + case CanMessagePosicaoDados.Dados4: + { + LogDiagnosticoI2CDados4(data); + break; + } + } + + if (!(Dados.DadosLeitura?.StatusI2C?.Any() ?? false)) + Dados.DadosLeitura.StatusI2C = new Dictionary(); + Dados.DadosLeitura.StatusI2C[posicao] = data; + + break; + } + case S_Code.sTOD: + { + switch (posicao) + { + case CanMessagePosicaoDados.DadosAll: + { + double latenciaLoop = ConverterByteParaDouble(data, 2) * 100; + Dados.DadosLeitura.LatenciaLoop = Convert.ToInt32(latenciaLoop); + //Console.WriteLine($"[SEN] Ciclo concluido. Latencia de loop: {latenciaLoop} ms"); + break; + } + } + break; + } + case S_Code.sNTC: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + ushort raw = Converter2BytesU16(data, 2); + double temperatura = ConverterTemperaturaNtc(data, 4); + double tensao = ConverterTensaoNtc(data, 6); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { raw, temperatura, tensao }); + //Variaveis.MostrarLog($"[SEN] sNTC | ID_Num: {ID_Num} | Posicao: {posicao} | Leitura: {raw} | Temperatura: {temperatura} | Tensao: {tensao}"); + break; + } + } + break; + } + case S_Code.sCOR: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + double busVoltage = ConverterByteParaDouble(data, 2); // continua igual + double current = ConverterByteParaDouble(data, 4) * 1000.0; // volts / 100, amps * 10 (volta para mA) + double power = ConverterByteParaDouble(data, 6) * 1000.0; // volta para mW + //Console.WriteLine($"[AB36V] Tensao {busVoltage}, Corrente: {current}, Power: {power}"); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { busVoltage, current, power }); + break; + } + case CanMessagePosicaoDados.Dados2: + { + double energia = ConverterByteParaDouble(data, 2) * 1000.0; // volta para mWh + double temperatura = ConverterByteParaDouble(data, 4); // volta para °C + double codAlarme = ConverterByteParaDouble(data, 6) * 100; // volta para °C + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { energia, temperatura, codAlarme }); + break; + } + } + break; + } + case S_Code.sLED: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + int statusId = data[2]; + StatusLED statusLeitura = (StatusLED)data[3]; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { statusId, statusLeitura }); + break; + } + } + break; + } + case S_Code.sRLE: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + Estado estado = (Estado)data[2]; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { estado }); + break; + } + } + break; + } + case S_Code.sSRV: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + int angulo = data[2]; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { angulo }); + break; + } + } + break; + } + case S_Code.sIMU: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + double roll = ConverterByteParaDouble(data, 2); + double pitch = ConverterByteParaDouble(data, 4); + double yaw = ConverterByteParaDouble(data, 6); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { roll, pitch, yaw }); + break; + } + case CanMessagePosicaoDados.Dados2: + { + double temperatura = ConverterByteParaDouble(data, 2); + double pressao = ConverterValorEscala(data, 4, 10); + double altitude = ConverterValorEscala(data, 6, 10); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { temperatura, pressao, altitude }); + break; + } + case CanMessagePosicaoDados.Dados3: + { + double accX = ConverterByteParaDouble(data, 2); + double accY = ConverterByteParaDouble(data, 4); + double accZ = ConverterByteParaDouble(data, 6); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { accX, accY, accZ }); + break; + } + case CanMessagePosicaoDados.Dados4: + { + double gyrX = ConverterByteParaDouble(data, 2); + double gyrY = ConverterByteParaDouble(data, 4); + double gyrZ = ConverterByteParaDouble(data, 6); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { gyrX, gyrY, gyrZ }); + break; + } + case CanMessagePosicaoDados.Dados5: + { + double magX = ConverterByteParaDouble(data, 2); + double magY = ConverterByteParaDouble(data, 4); + double magZ = ConverterByteParaDouble(data, 6); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { magX, magY, magZ }); + break; + } + } + break; + } + case S_Code.sGAS: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + ushort raw = Converter2BytesU16(data, 2); + double ppm = ConverterBytesParaDouble32(data, 4); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { raw, ppm }); + break; + } + } + break; + } + case S_Code.sSHT: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + double temperatura = ConverterByteParaDouble(data, 2); + double umidade = ConverterByteParaDouble(data, 4); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { temperatura, umidade }); + break; + } + } + break; + } + case S_Code.sLUZ: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + double lux = ConverterBytesParaDouble32(data, 2); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { lux }); + break; + } + } + break; + } + case S_Code.sIUV: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + ushort raw = Converter2BytesU16(data, 2); + double iuv = ConverterByteParaDouble(data, 4); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { raw, iuv }); + break; + } + } + break; + } + case S_Code.sQAR: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + double tvoc = ConverterByteParaDouble(data, 2); + double eco2 = ConverterByteParaDouble(data, 4); + int aqi = data[6]; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { tvoc, eco2, aqi }); + break; + } + case CanMessagePosicaoDados.Dados2: + { + double temperatura = ConverterByteParaDouble(data, 2); + double umidade = ConverterByteParaDouble(data, 4); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { temperatura, umidade }); + break; + } + } + break; + } + case S_Code.sCHV: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + ushort raw = Converter2BytesU16(data, 2); + double percentual = ConverterByteParaDouble(data, 4); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { raw, percentual }); + break; + } + } + break; + } + case S_Code.sMAG: + { + switch (posicao) + { + case CanMessagePosicaoDados.Status: + { + bool Iniciado = data[2] == 1; + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { Iniciado }); + break; + } + case CanMessagePosicaoDados.Dados1: + { + ushort raw = Converter2BytesU16(data, 2); + double angle = ConverterAngle100(data, 4); + AtualizarDados(Dados, ID_Num, posicao, componente, new List() { raw, angle }); + //Console.WriteLine($"[sMAG] {ID_Num}, Raw: {raw}, Angulo: {angle}"); + break; + } + } + break; + } + } + + DateTime respostaEm = DateTime.Now; + Dados.DadosLeitura.Momento = respostaEm; + Dados.DadosLeitura.UltimoComandoRespondido = respostaEm; + + if (componente == S_Code.sMOD) + Dados.ConfirmarMapeamentoAposDescoberta(respostaEm); + } + catch (Exception ex) + { + LogFrameInvalido( + $"erro ao processar {componente}/{ID_Num}/{posicao}: " + + $"{ex.GetType().Name}: {ex.Message}" + ); + return; + } + } + + var opAtual = Variaveis.OperacaoEmAndamento; + if (!ReferenceEquals(opAtual?.DispSen?.Dados, Dados)) + { + LogFrameInvalido("snapshot do SEN substituído durante o processamento"); + return; + } + + try + { + opAtual?.DispMvd?.Dados?.AtualizarDadosSensoresExternos(componente, ID_Num); + } + catch (Exception ex) + { + Variaveis.MostrarLog( + $"[SEN/RX] Falha ao propagar sensor externo {componente}/{ID_Num}: {ex}" + ); + } } } diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs index 98e6e2d9d..8686b9426 100644 --- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs +++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs @@ -4699,6 +4699,7 @@ namespace AgroBase.Models model.RequisitarDados = dados.RequisitarDados; model.LatenciaLoopMs = leitura?.LatenciaLoop ?? 0; + model.StatusI2C = new Dictionary(leitura?.StatusI2C ?? new Dictionary()); model.UltimoComandoRespondido = leitura?.UltimoComandoRespondido ?? DateTime.MinValue; model.IdadeUltimoComandoMs = IdadeMs(model.UltimoComandoRespondido); @@ -6132,6 +6133,7 @@ namespace AgroBase.Models public bool RequisitarDados { get; set; } public int LatenciaLoopMs { get; set; } + public Dictionary StatusI2C { get; set; } public DateTime UltimoComandoRespondido { get; set; } = DateTime.MinValue; public double IdadeUltimoComandoMs { get; set; } @@ -6196,6 +6198,7 @@ namespace AgroBase.Models RequisitarDados = RequisitarDados, LatenciaLoopMs = LatenciaLoopMs, + StatusI2C = new Dictionary(StatusI2C ?? new Dictionary()), UltimoComandoRespondido = UltimoComandoRespondido, IdadeUltimoComandoMs = IdadeUltimoComandoMs, diff --git a/AgroBase/AgroBase/Services/CanServiceEthernet.cs b/AgroBase/AgroBase/Services/CanServiceEthernet.cs index 146e5175f..922ecb128 100644 --- a/AgroBase/AgroBase/Services/CanServiceEthernet.cs +++ b/AgroBase/AgroBase/Services/CanServiceEthernet.cs @@ -315,8 +315,7 @@ namespace AgroBase.Services catch (Exception ex) { MostrarLog($"Erro ao inicializar: {ex.Message}"); - IsConnected = false; - Iniciado = false; + MarcarTransporteIndisponivel(); return false; } } @@ -326,23 +325,41 @@ namespace AgroBase.Services try { _tcpClient = new TcpClient(); + _tcpClient.NoDelay = true; _tcpClient.ReceiveBufferSize = 64 * 1024; _tcpClient.SendBufferSize = 64 * 1024; var task = _tcpClient.ConnectAsync(IpAddress, Port); - if (!task.Wait(1200)) - return false; - if (!_tcpClient.Connected) + if (!task.Wait(1200) || !_tcpClient.Connected) + { + try { _tcpClient.Close(); } + catch { } + + _tcpClient = null; + _tcpStream = null; + return false; + } _tcpStream = _tcpClient.GetStream(); + return true; } catch (Exception ex) { + try { _tcpStream?.Close(); } + catch { } + + try { _tcpClient?.Close(); } + catch { } + + _tcpStream = null; + _tcpClient = null; + MostrarLog($"Erro TCP: {ex.Message}"); + return false; } } @@ -368,18 +385,15 @@ namespace AgroBase.Services private void DefinirDispositivo() { - if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Can)) + if (!IsConnected || !Iniciado) + return; + + SerialService.AdicionarOuAtualizarDispositivoMapeado(new DispositivoDetalhesModel { - if (IsConnected && Iniciado) - { - SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel() - { - Dispositivo = T_Code.Can, - Endereco = IpAddress, - Versao = ("1").ToString() - }); - } - } + Dispositivo = T_Code.Can, + Endereco = IpAddress, + Versao = "1" + }); } public bool AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true, bool contabilizarTimeoutHealth = true, bool mensagemDescoberta = false) @@ -599,7 +613,7 @@ namespace AgroBase.Services if (!EnviarPacote(packet, msg.IdTx)) { MostrarLog($"Erro ao enviar frame ID=0x{msg.IdTx:X}", msg.IdTx); - IsConnected = false; + MarcarTransporteIndisponivel(); break; } @@ -695,7 +709,7 @@ namespace AgroBase.Services catch (Exception ex) { MostrarLog($"Erro RX: {ex.Message}"); - IsConnected = false; + MarcarTransporteIndisponivel(); } await Task.CompletedTask; @@ -703,54 +717,71 @@ namespace AgroBase.Services private void OuvirTcp() { - if (_tcpClient == null || _tcpStream == null || !_tcpClient.Connected) + lock (_socketLock) { - IsConnected = false; - return; - } + if (_tcpClient == null || _tcpStream == null || !_tcpClient.Connected) + { + MarcarTransporteIndisponivel(); + return; + } - while (_tcpStream.DataAvailable) - { - byte[] buffer = new byte[4096]; - int lidos = _tcpStream.Read(buffer, 0, buffer.Length); - if (lidos <= 0) - break; + while (_tcpStream.DataAvailable) + { + byte[] buffer = new byte[4096]; - for (int i = 0; i < lidos; i++) - TcpRxBuffer.Add(buffer[i]); - } + int lidos = _tcpStream.Read(buffer, 0, buffer.Length); - while (TcpRxBuffer.Count >= USR_FRAME_SIZE) - { - byte[] packet = TcpRxBuffer.Take(USR_FRAME_SIZE).ToArray(); - TcpRxBuffer.RemoveRange(0, USR_FRAME_SIZE); - ProcessarPacoteUsrStandard(packet); + if (lidos <= 0) + break; + + for (int i = 0; i < lidos; i++) + TcpRxBuffer.Add(buffer[i]); + } + + while (TcpRxBuffer.Count >= USR_FRAME_SIZE) + { + byte[] packet = TcpRxBuffer.Take(USR_FRAME_SIZE).ToArray(); + + TcpRxBuffer.RemoveRange(0, USR_FRAME_SIZE); + + ProcessarPacoteUsrStandard(packet); + } } } private void OuvirUdp() { - if (_udpClient == null) + lock (_socketLock) { - IsConnected = false; - return; - } - - while (_udpClient.Available > 0) - { - IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0); - byte[] pacote = _udpClient.Receive(ref ep); - - if (pacote == null || pacote.Length == 0) - continue; - - int offset = 0; - while (offset + USR_FRAME_SIZE <= pacote.Length) + if (_udpClient == null) { - byte[] frame = new byte[USR_FRAME_SIZE]; - Buffer.BlockCopy(pacote, offset, frame, 0, USR_FRAME_SIZE); - ProcessarPacoteUsrStandard(frame); - offset += USR_FRAME_SIZE; + MarcarTransporteIndisponivel(); + return; + } + + while (_udpClient.Available > 0) + { + IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0); + + byte[] pacote = _udpClient.Receive(ref ep); + + if (pacote == null || pacote.Length == 0) + { + continue; + } + + int offset = 0; + + while (offset + USR_FRAME_SIZE <= pacote.Length) + { + byte[] frame = new byte[USR_FRAME_SIZE]; + + Buffer.BlockCopy(pacote, offset, frame, 0, USR_FRAME_SIZE); + + ProcessarPacoteUsrStandard(frame); + + offset += USR_FRAME_SIZE; + } } } } @@ -1142,8 +1173,8 @@ namespace AgroBase.Services DateTime agora = DateTime.Now; bool socketOk = TransportMode == CanEthernetTransportMode.Tcp - ? (_tcpClient != null && _tcpClient.Connected && _tcpStream != null) - : (_udpClient != null); + ? TcpSocketSaudavel() + : _udpClient != null; double ageTx = _ultimoTx == DateTime.MinValue ? 999999 @@ -1159,13 +1190,11 @@ namespace AgroBase.Services _socketIndisponivelDesde = agora; double tempoSuspeito = (agora - _socketIndisponivelDesde).TotalSeconds; - - bool semTrafegoRecente = ageTx > 2.0 && ageRx > 2.0; - - if (tempoSuspeito >= 1.0 && semTrafegoRecente) + if (tempoSuspeito >= 1.0 && (IsConnected || Iniciado)) { - IsConnected = false; + MarcarTransporteIndisponivel(); RegistrarErroCritico("socket_indisponivel_confirmado"); + MostrarLog("Socket CAN-ETH indisponível; aguardando reinicialização do transporte..."); } return; @@ -1174,25 +1203,50 @@ namespace AgroBase.Services _socketIndisponivelDesde = DateTime.MinValue; bool existeDemanda = ExisteDemandaCan(); - - // Se existe demanda, mas houve TX recente e não houve RX, - // isso parece mais "dispositivo CAN não respondeu" do que "socket morreu". - // Deixa o timeout da fila e o health tratarem. - if (IsConnected && existeDemanda && ageTx > 10.0 && ageRx > 10.0) + bool txAtivo = ageTx <= 2.0 || FreqTxJanela > 0.5; + if (IsConnected && existeDemanda && txAtivo && ageRx > 8.0) { - RegistrarErroCritico("sem_trafego_prolongado_com_demanda"); - MostrarLog("Sem tráfego prolongado no CAN-ETH com demanda, reiniciando conexão..."); + RegistrarErroCritico("tx_ativo_sem_rx_prolongado"); + MostrarLog("TX ativo sem RX prolongado; reiniciando conexão CAN-ETH..."); ReabrirConexao(); } - if (_ultimoErroCritico != DateTime.MinValue && - (agora - _ultimoErroCritico).TotalMinutes > 10) + if (_ultimoErroCritico != DateTime.MinValue && (agora - _ultimoErroCritico).TotalMinutes > 10) { ErrosCriticos = 0; } } - private void ReabrirConexao() + private bool TcpSocketSaudavel() + { + try + { + if (_tcpClient == null || + _tcpClient.Client == null || + _tcpStream == null || + !_tcpClient.Connected) + { + return false; + } + + Socket socket = _tcpClient.Client; + + bool remotoFechou = + socket.Poll( + 0, + SelectMode.SelectRead + ) && + socket.Available == 0; + + return !remotoFechou; + } + catch + { + return false; + } + } + + private bool ReabrirConexao() { try { @@ -1200,15 +1254,23 @@ namespace AgroBase.Services FecharConexaoSemLimparHandlers(); - if (TransportMode == CanEthernetTransportMode.Tcp) - IsConnected = ConectarTcp(); - else - IsConnected = ConectarUdp(); + bool conectado = TransportMode == CanEthernetTransportMode.Tcp + ? ConectarTcp() + : ConectarUdp(); - Iniciado = IsConnected; + IsConnected = conectado; + Iniciado = conectado; + + if (!conectado) + { + MostrarLog($"Reconexão CAN-ETH falhou em {IpAddress}:{Port}"); + return false; + } ReconexoesTotal++; + _socketIndisponivelDesde = DateTime.MinValue; + _ultimoRx = DateTime.Now; _ultimoTx = DateTime.Now; @@ -1217,11 +1279,16 @@ namespace AgroBase.Services UltimoRxUnixMs = NowUnixMs(); UltimoTxUnixMs = NowUnixMs(); + + MostrarLog($"CAN-ETH reconectado em {IpAddress}:{Port}"); + + return true; } - catch + catch (Exception ex) { - IsConnected = false; - Iniciado = false; + MarcarTransporteIndisponivel(); + MostrarLog($"Erro ao reabrir CAN-ETH: {ex.Message}"); + return false; } } @@ -1260,6 +1327,12 @@ namespace AgroBase.Services Iniciado = false; } + private void MarcarTransporteIndisponivel() + { + IsConnected = false; + Iniciado = false; + } + public void Fechar() { tmrOuvirCAN?.Dispose(); diff --git a/AgroBase/AgroBase/Services/MKS057DCanService.cs b/AgroBase/AgroBase/Services/MKS057DCanService.cs index 985c02b8b..b65a44d10 100644 --- a/AgroBase/AgroBase/Services/MKS057DCanService.cs +++ b/AgroBase/AgroBase/Services/MKS057DCanService.cs @@ -445,14 +445,14 @@ namespace AgroBase.Services if (leitura == null) continue; - SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel() + SerialService.AdicionarOuAtualizarDispositivoMapeado(new DispositivoDetalhesModel() { Dispositivo = T_Code.Dir, Endereco = CanManager.CanService._portName, Versao = (leitura.FV ?? "").ToString(), Mod_ID = modId, }); - + op.Sensoriamento.Operacao.ModsCalibragem.TryGetValue(T_Code.Dir, out var m); if (m == null) diff --git a/AgroBase/AgroBase/Services/OIDCanService.cs b/AgroBase/AgroBase/Services/OIDCanService.cs index dba6f4781..2482704c4 100644 --- a/AgroBase/AgroBase/Services/OIDCanService.cs +++ b/AgroBase/AgroBase/Services/OIDCanService.cs @@ -456,7 +456,7 @@ namespace AgroBase.Services if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Mov && x.Mod_ID == modId)) { - SerialService.DispositivosMapeados.Add( + SerialService.AdicionarOuAtualizarDispositivoMapeado( new DispositivoDetalhesModel { Dispositivo = T_Code.Mov, diff --git a/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs b/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs index 3c05895c6..1b6c5d4f9 100644 --- a/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs +++ b/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs @@ -164,7 +164,7 @@ namespace AgroBase.Services.Operadores double pressaoAlvo = bombaLinha?._PressaoSPControle > 0 ? bombaLinha._PressaoSPControle : op.Parametros?.Controle?.AtuPressaoLinha ?? 0; double pressaoLinha = bombaLinha?._PressaoAtual ?? _Sensoriamento?.Atuador?.PressaoLinha ?? 0; double toleranciaPressao = Math.Max(2.0, pressaoAlvo * 0.15); - bool pressaoLinhaRegular = pressaoAlvo <= 0 || Math.Abs(pressaoLinha - pressaoAlvo) <= toleranciaPressao; + bool pressaoLinhaRegular = pressaoAlvo <= 0 || pressaoLinha >= Math.Max(0, pressaoAlvo - toleranciaPressao); RedisService.AtualizarCampos( CtxKey.DadosContexto, @@ -222,7 +222,7 @@ namespace AgroBase.Services.Operadores ("Atuador", new { pressao_linha_regular = pressaoLinhaRegular, - pressao_linha = _Sensoriamento.Atuador?.PressaoLinha ?? 0, + pressao_linha = pressaoLinha, potencia_bomba = _Sensoriamento.Atuador?.LeituraPotenciaBomba ?? 0, estado_bomba = _Sensoriamento.Atuador?.LeituraComandoBomba ?? false, tempo_bomba_atuada = _Sensoriamento.Atuador?.TempoBombaAtuada ?? 0, @@ -537,6 +537,17 @@ namespace AgroBase.Services.Operadores var agora_dt = DateTime.Now; var agora = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + double CalcularIdadeMs(DateTime instante) + { + if (instante == DateTime.MinValue) + return 999999; + + return Math.Max( + 0, + (agora_dt - instante).TotalMilliseconds + ); + } + //Console.WriteLine(RedisService.Get(RedisService.ModKey(Enums.T_Code.Gps))); //Console.WriteLine(RedisService.Get(RedisService.ModKey(Enums.T_Code.Dir))); //Console.WriteLine(RedisService.Get(RedisService.ModKey(Enums.T_Code.Mov))); @@ -547,6 +558,8 @@ namespace AgroBase.Services.Operadores //RedisService.SetObject(RedisService.ModKey(Enums.T_Code.Mov), string.Empty); //RedisService.SetObject(RedisService.ModKey(Enums.T_Code.Sen), string.Empty); + #region MOV e DIR + var dirAtivos = new List(); var movAtivos = new List(); @@ -700,18 +713,20 @@ namespace AgroBase.Services.Operadores RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Dir), ("ativos", dirAtivos.ToList())); RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Mov), ("ativos", movAtivos.ToList())); + #endregion + #region SEN var DadosSen = op.DispSen.Dados; DateTime ultimaRespostaSen = DadosSen.DadosLeitura?.UltimoComandoRespondido ?? DateTime.MinValue; - double idadeUltimaRespostaSenMs = - ultimaRespostaSen == DateTime.MinValue - ? 999999 - : (agora_dt - ultimaRespostaSen).TotalMilliseconds; + double idadeUltimaRespostaSenMs = CalcularIdadeMs(ultimaRespostaSen); + RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Sen), ("timestamp", agora), ("conectado", DadosSen.Conectado), + ("telemetria_recente", DadosSen.TelemetriaRecente), + ("firmware_conectado", DadosSen.FirmwareDeclarouConectado), ("freq_base", DadosSen.FrequenciaBase), ("bms_ligado", _Sensoriamento.Bateria?.BmsLigado ?? false), @@ -743,10 +758,7 @@ namespace AgroBase.Services.Operadores }) .ToList(); - double idadeUltimaRespostaSensorMs = - sensor.UltimaLeitura == DateTime.MinValue - ? 999999 - : (agora_dt - sensor.UltimaLeitura).TotalMilliseconds; + double idadeUltimaRespostaSensorMs = CalcularIdadeMs(sensor.UltimaLeitura); var sensorObj = new { @@ -754,6 +766,7 @@ namespace AgroBase.Services.Operadores id, label = sensor.ID, tipo, + freq_base = DadosSen.FrequenciaBase, aferir = sensor.Aferir, mandatorio = sensor.Mandatorio, dados, @@ -799,15 +812,8 @@ namespace AgroBase.Services.Operadores bool possuiRespostaServo = ultimaRespostaAnguloServo != DateTime.MinValue; - double idadeUltimoComandoServoMs = - !possuiComandoServo - ? 999999 - : Math.Max(0, (agora_dt - servo.UltimoComandoEnviado).TotalMilliseconds); - - double idadeUltimaRespostaServoMs = - !possuiRespostaServo - ? 999999 - : Math.Max(0, (agora_dt - ultimaRespostaAnguloServo).TotalMilliseconds); + double idadeUltimoComandoServoMs = CalcularIdadeMs(servo.UltimoComandoEnviado); + double idadeUltimaRespostaServoMs = CalcularIdadeMs(ultimaRespostaAnguloServo); bool telemetriaPosComandoServo = possuiComandoServo && @@ -820,6 +826,7 @@ namespace AgroBase.Services.Operadores id, label = servo.ID, tipo, + freq_base = DadosSen.FrequenciaBase, controlar = servo.Controlar, mandatorio = servo.Mandatorio, @@ -875,15 +882,8 @@ namespace AgroBase.Services.Operadores bool possuiRespostaRele = ultimaRespostaEstadoRele != DateTime.MinValue; - double idadeUltimoComandoReleMs = - !possuiComandoRele - ? 999999 - : Math.Max(0, (agora_dt - rele.UltimoComandoEnviado).TotalMilliseconds); - - double idadeUltimaRespostaReleMs = - !possuiRespostaRele - ? 999999 - : Math.Max(0, (agora_dt - ultimaRespostaEstadoRele).TotalMilliseconds); + double idadeUltimoComandoReleMs = CalcularIdadeMs(rele.UltimoComandoEnviado); + double idadeUltimaRespostaReleMs = CalcularIdadeMs(ultimaRespostaEstadoRele); bool telemetriaPosComandoRele = possuiComandoRele && @@ -896,6 +896,7 @@ namespace AgroBase.Services.Operadores id, label = rele.ID, tipo, + freq_base = DadosSen.FrequenciaBase, controlar = rele.Controlar, mandatorio = rele.Mandatorio, dados, @@ -966,15 +967,8 @@ namespace AgroBase.Services.Operadores ? (ultimaRespostaLedId <= ultimaRespostaLedStatus ? ultimaRespostaLedId : ultimaRespostaLedStatus) : DateTime.MinValue; - double idadeUltimoComandoLedMs = - !possuiComandoLed - ? 999999 - : Math.Max(0, (agora_dt - led.UltimoComandoEnviado).TotalMilliseconds); - - double idadeUltimaRespostaLedMs = - !possuiRespostaLed - ? 999999 - : Math.Max(0, (agora_dt - ultimaRespostaLed).TotalMilliseconds); + double idadeUltimoComandoLedMs = CalcularIdadeMs(led.UltimoComandoEnviado); + double idadeUltimaRespostaLedMs = CalcularIdadeMs(ultimaRespostaLed); bool telemetriaPosComandoLed = possuiComandoLed && @@ -990,6 +984,7 @@ namespace AgroBase.Services.Operadores id, label = led.ID, tipo, + freq_base = DadosSen.FrequenciaBase, controlar = led.Controlar, mandatorio = led.Mandatorio, @@ -1018,27 +1013,22 @@ namespace AgroBase.Services.Operadores senLedsAtualizados.Add(("sinaleiros.ativos", senLedsAtivos)); RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Sen), senLedsAtualizados.ToArray()); + #endregion + #region ATU var DadosAtu = op.DispAtu.Dados; DateTime ultimaRespostaAtu = DadosAtu.DadosLeitura?.UltimoComandoRespondido ?? DateTime.MinValue; - double idadeUltimaRespostaAtuMs = - ultimaRespostaAtu == DateTime.MinValue - ? 999999 - : (agora_dt - ultimaRespostaAtu).TotalMilliseconds; + double idadeUltimaRespostaAtuMs = CalcularIdadeMs(ultimaRespostaAtu); - bool pulverizadorAutomatico = - op.Parametros?.Controle?.PulverizadorAutomatico ?? false; + bool pulverizadorAutomatico = op.Parametros?.Controle?.PulverizadorAutomatico ?? false; - double pressaoAlvo = - op.Parametros?.Controle?.AtuPressaoLinha ?? 0; + double pressaoAlvo = op.Parametros?.Controle?.AtuPressaoLinha ?? 0; - double pressaoLinha = - DadosAtu.PressaoLinha; - - bool pressaoLinhaRegular = - pressaoAlvo <= 0 || pressaoLinha >= pressaoAlvo - 20; + double pressaoLinha = DadosAtu?.BombaPressurizadora?._PressaoAtual ?? _Sensoriamento?.Atuador?.PressaoLinha ?? 0; + double toleranciaPressao = Math.Max(2.0, pressaoAlvo * 0.15); + bool pressaoLinhaRegular = pressaoAlvo <= 0 || pressaoLinha >= Math.Max(0, pressaoAlvo - toleranciaPressao); RedisService.AtualizarCampos( RedisService.ModKey(Enums.T_Code.Atu), ("timestamp", agora), @@ -1091,10 +1081,7 @@ namespace AgroBase.Services.Operadores }) .ToList(); - double idadeUltimaRespostaSensorMs = - sensor.UltimaLeitura == DateTime.MinValue - ? 999999 - : (agora_dt - sensor.UltimaLeitura).TotalMilliseconds; + double idadeUltimaRespostaSensorMs = CalcularIdadeMs(sensor.UltimaLeitura); var sensorObj = new { @@ -1107,6 +1094,7 @@ namespace AgroBase.Services.Operadores dados, timestamp = agora, saude = saude ?? new { }, + freq_base = DadosAtu.FrequenciaBase, ultima_resposta_ms = idadeUltimaRespostaSensorMs, }; @@ -1139,15 +1127,11 @@ namespace AgroBase.Services.Operadores }) .ToList(); - double idadeUltimoComandoBicoMs = - bico.UltimoComandoEnviado == DateTime.MinValue - ? 999999 - : (agora_dt - bico.UltimoComandoEnviado).TotalMilliseconds; + DateTime ultimoComandoBico = bico.UltimoComandoEnfileirado; - double idadeUltimaRespostaBicoMs = - bico.UltimaLeitura == DateTime.MinValue - ? 999999 - : (agora_dt - bico.UltimaLeitura).TotalMilliseconds; + double idadeUltimoComandoBicoMs = CalcularIdadeMs(ultimoComandoBico); + + double idadeUltimaRespostaBicoMs = CalcularIdadeMs(bico.UltimaLeitura); var bicoObj = new { @@ -1160,6 +1144,7 @@ namespace AgroBase.Services.Operadores dados, timestamp = agora, saude = saude ?? new { }, + freq_base = DadosAtu.FrequenciaBase, comando_atuar = bico.ComandoAtuar, estado_desejado = bico.UltimoStatusDesejado, @@ -1181,15 +1166,15 @@ namespace AgroBase.Services.Operadores vazao_media_mls = bico.VazaoMediaMLs, volume_vazado_ml = bico.VolumeVazadoML, - comando_solicitado = bico.ComandoAtuar, + comando_solicitado = bico.Comandar && bico.ComandoAtuar, status_desejado_fisico = bico.UltimoStatusDesejado, angulo_desejado_fisico = bico.UltimoAnguloDesejado, ultimo_comando_ms = idadeUltimoComandoBicoMs, ultima_resposta_ms = idadeUltimaRespostaBicoMs, telemetria_pos_comando = - bico.UltimoComandoEnviado != DateTime.MinValue && - bico.UltimaLeitura != DateTime.MinValue && - bico.UltimaLeitura >= bico.UltimoComandoEnviado + ultimoComandoBico != DateTime.MinValue && + bico.UltimaRespostaPosteriorAoComando != DateTime.MinValue && + bico.UltimaRespostaPosteriorAoComando >= ultimoComandoBico }; atuBicosAtualizados.Add(($"{basePath}", bicoObj)); @@ -1221,15 +1206,11 @@ namespace AgroBase.Services.Operadores }) .ToList(); - double idadeUltimoComandoBombaMs = - bomba.UltimoComandoEnviado == DateTime.MinValue - ? 999999 - : (agora_dt - bomba.UltimoComandoEnviado).TotalMilliseconds; + DateTime ultimoComandoBomba = bomba.UltimoComandoEnfileirado; - double idadeUltimaRespostaBombaMs = - bomba.UltimaLeitura == DateTime.MinValue - ? 999999 - : (agora_dt - bomba.UltimaLeitura).TotalMilliseconds; + double idadeUltimoComandoBombaMs = CalcularIdadeMs(ultimoComandoBomba); + + double idadeUltimaRespostaBombaMs = CalcularIdadeMs(bomba.UltimaLeitura); var sensorObj = new { @@ -1242,6 +1223,7 @@ namespace AgroBase.Services.Operadores dados, timestamp = agora, saude = saude ?? new { }, + freq_base = DadosAtu.FrequenciaBase, comando_atuar = bomba.ComandoAtuar, estado_desejado = bomba.UltimoStatusDesejado, @@ -1273,13 +1255,13 @@ namespace AgroBase.Services.Operadores pressao_desejada_fisica = bomba.UltimaPressaoDesejada, ultimo_comando_ms = idadeUltimoComandoBombaMs, ultima_resposta_ms = idadeUltimaRespostaBombaMs, - telemetria_pos_comando = - bomba.UltimoComandoEnviado != DateTime.MinValue && - bomba.UltimaLeitura != DateTime.MinValue && - bomba.UltimaLeitura >= bomba.UltimoComandoEnviado, - + telemetria_pos_comando = + ultimoComandoBomba != DateTime.MinValue && + bomba.UltimaRespostaPosteriorAoComando != DateTime.MinValue && + bomba.UltimaRespostaPosteriorAoComando >= ultimoComandoBomba, desligar_sem_bicos_abertos = bomba.Controle?.DesligarSemBicosAbertos ?? true, qtd_bicos_ligados = bomba._QtdBicosLigados, + potencia_final = bomba._PotenciaFinal, potencia_feedforward = bomba._PotenciaFeedForwared, pressao_sensor = bomba._PressaoSensor, @@ -1300,6 +1282,7 @@ namespace AgroBase.Services.Operadores atuBombasAtualizados.Add(("bombas.ativos", atuBombasAtivos)); RedisService.AtualizarCampos(RedisService.ModKey(Enums.T_Code.Atu), atuBombasAtualizados.ToArray()); + #endregion //var DadosLra = Variaveis.LoraService; diff --git a/AgroBase/AgroBase/Services/SerialService.cs b/AgroBase/AgroBase/Services/SerialService.cs index d85688a2b..83bf02bf1 100644 --- a/AgroBase/AgroBase/Services/SerialService.cs +++ b/AgroBase/AgroBase/Services/SerialService.cs @@ -785,7 +785,7 @@ namespace AgroBase.Services return false; } - if (!CanManager.CanService.Iniciado) + if (!CanManager.CanService.Iniciado || !CanManager.CanService.IsConnected) { bool initialized = CanManager.CanService.Inicializar(250); @@ -1587,8 +1587,7 @@ namespace AgroBase.Services } } - public static void AdicionarOuAtualizarDispositivoMapeado( - DispositivoDetalhesModel device) + public static void AdicionarOuAtualizarDispositivoMapeado(DispositivoDetalhesModel device) { if (device == null) return; diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/atuador.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/atuador.py index 115de31e8..8b7db1863 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/atuador.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/atuador.py @@ -56,6 +56,7 @@ class ModuloAtuador(ModuloDiagnosticoBase): self.padroes_bicos = {} self.padroes_bombas = {} self.runtime_bombas = {} + self.runtime_bicos = {} def atualizar_saude(self): try: @@ -612,50 +613,149 @@ class ModuloAtuador(ModuloDiagnosticoBase): dados = item["dados"] label = dados.get("label") + agora = time.monotonic() + runtime = self._runtime_bico(item) + + # ============================================================ + # Comando desejado e estado físico + # ============================================================ + comando_presente = ( - "comando_atuar" in dados or - "estado_desejado" in dados or - "status_desejado_fisico" in dados or - "ultimo_status_desejado" in dados + "comando_atuar" in dados + or "estado_desejado" in dados + or "status_desejado_fisico" in dados + or "ultimo_status_desejado" in dados ) comando = self._estado_fisico_desejado_ligado(dados) - estado = self._get(dados, "estado_leitura", self._campo_atual(dados, "EstadoLeitura", 0)) + + estado = self._get( + dados, + "estado_leitura", + self._campo_atual( + dados, + "EstadoLeitura", + 0, + ), + ) + estado_on = self._estado_ligado(estado) - usa_servo = dados.get("com_movimento", False) + # ============================================================ + # Controle angular, quando o bico possui movimento + # ============================================================ + + usa_servo = self._bool( + dados.get("com_movimento", False) + ) angulo = None angulo_desejado = None angulo_neutro = None angulo_controle = None + erro_angulo = 0.0 if usa_servo: angulo = self._float( - self._get(dados, "angulo_leitura", self._campo_atual(dados, "ServoAnguloLeitura", 0)), + self._get( + dados, + "angulo_leitura", + self._campo_atual( + dados, + "ServoAnguloLeitura", + 0, + ), + ), 0, ) - angulo_neutro = self._float(dados.get("angulo_neutro", 90), 90) - angulo_controle = self._float(dados.get("angulo_controle", dados.get("angulo_sp", 90)), 90) + angulo_neutro = self._float( + dados.get("angulo_neutro", 90), + 90, + ) + + angulo_controle = self._float( + dados.get( + "angulo_controle", + dados.get("angulo_sp", 90), + ), + 90, + ) angulo_desejado = self._float( dados.get( "angulo_desejado_fisico", dados.get( "angulo_desejado", - angulo_controle if comando else angulo_neutro - ) + ( + angulo_controle + if comando + else angulo_neutro + ), + ), + ), + ( + angulo_controle + if comando + else angulo_neutro ), - angulo_controle if comando else angulo_neutro, ) - ultimo_comando_ms = self._float(dados.get("ultimo_comando_ms", 999999), 999999) - tem_idade_comando = ultimo_comando_ms < 900000 + erro_angulo = abs( + angulo - angulo_desejado + ) - score = 100.0 + # ============================================================ + # Informações temporais + # ============================================================ + + ultimo_comando_ms = self._float( + dados.get("ultimo_comando_ms", 999999), + 999999, + ) + + ultima_resposta_ms = self._obter_ultima_resposta_ms( + dados + ) + + tem_comando_fisico = ( + self._tem_comando_fisico_registrado(dados) + ) + + telemetria_fresca = ( + ultima_resposta_ms <= self.RESPOSTA_RECENTE_MS + ) + + telemetria_pos_comando = ( + self._telemetria_depois_do_comando(dados) + ) + + # ============================================================ + # Sem informações suficientes sobre o comando + # ============================================================ if not comando_presente: + self._atualizar_persistencia( + runtime, + "comandado_on_leitura_off_desde", + False, + agora, + ) + + self._atualizar_persistencia( + runtime, + "comandado_off_leitura_on_desde", + False, + agora, + ) + + self._atualizar_persistencia( + runtime, + "angulo_divergente_desde", + False, + agora, + ) + return 92, { "comando_disponivel": False, "estado_on": estado_on, @@ -664,103 +764,367 @@ class ModuloAtuador(ModuloDiagnosticoBase): "angulo_desejado": angulo_desejado, } - if tem_idade_comando and ultimo_comando_ms < self.COMANDO_GRACE_MS: - return 96, { - "comando_atuar": comando, - "estado_on": estado_on, - "usa_servo": usa_servo, - "angulo": angulo, - "angulo_desejado": angulo_desejado, - "ultimo_comando_ms": ultimo_comando_ms, - } - - if self._aguardando_telemetria_pos_comando(dados): - return 94, { - "comando_disponivel": True, - "aguardando_telemetria_pos_comando": True, - "comando_fisico": comando, - "comando_logico": self._comando_logico_ligado(dados), - "estado_on": estado_on, - "usa_servo": usa_servo, - "angulo": angulo, - "angulo_desejado": angulo_desejado, - "ultimo_comando_ms": ultimo_comando_ms, - "ultima_resposta_ms": self._float(dados.get("ultima_resposta_ms", 999999), 999999), - "telemetria_pos_comando": self._telemetria_depois_do_comando(dados), - } + # ============================================================ + # Telemetria velha + # + # A avaliação de resposta do componente já diagnosticará isso. + # Não devemos transformar uma leitura antiga em falha mecânica. + # ============================================================ - if comando and not estado_on: - severidade = 85 if tem_idade_comando and ultimo_comando_ms >= self.COMANDO_FALHA_MS else 70 - - motivos.append(f"Bico comandado ligado, mas leitura ainda indica desligado ({label})") - condicoes.append({ - "label": "Bico", - "valor": 0, - "severidade": severidade, - "classe": "processo", - "descricao": f"Bico {label} comandado ligado, mas estado lido ainda está desligado", - "acoes": [ - "Aguardar próxima atualização CAN.", - "Verificar solenoide do bico se a divergência persistir.", - "Conferir alimentação e retorno do bico.", - ], - }) - - score = min(score, 55 if severidade >= 85 else 75) - - if not comando and estado_on: - falha_confirmada = ( - tem_idade_comando and - ultimo_comando_ms >= self.COMANDO_FALHA_MS and - self._telemetria_depois_do_comando(dados) + if not telemetria_fresca: + self._atualizar_persistencia( + runtime, + "comandado_on_leitura_off_desde", + False, + agora, ) - severidade = 95 if falha_confirmada else 82 + self._atualizar_persistencia( + runtime, + "comandado_off_leitura_on_desde", + False, + agora, + ) - motivos.append(f"Bico ligado sem comando ({label})") - condicoes.append({ - "label": "Bico", - "valor": 1, - "severidade": severidade, - "classe": "falha_hardware" if falha_confirmada else "processo", - "descricao": f"Bico {label} aparenta estar ligado sem comando", - "acoes": [ - "Verificar travamento da solenoide.", - "Verificar se o comando de desligamento chegou ao ATU.", - ], - }) + self._atualizar_persistencia( + runtime, + "angulo_divergente_desde", + False, + agora, + ) - score = min(score, 25 if falha_confirmada else 65) + return 100, { + "comando_disponivel": True, + "avaliacao_suspensa": ( + "telemetria do bico desatualizada" + ), + "comando_fisico": comando, + "estado_on": estado_on, + "ultima_resposta_ms": ultima_resposta_ms, + } - if usa_servo and angulo is not None and angulo_desejado is not None: - erro_ang = abs(angulo - angulo_desejado) + # ============================================================ + # Divergências brutas + # ============================================================ - if erro_ang > self.ANGULO_TOL_BICO_GRAUS and (comando or estado_on): - motivos.append(f"Ângulo do bico distante do desejado ({angulo:.1f}°/{angulo_desejado:.1f}°)") - condicoes.append({ - "label": "Ângulo bico", - "valor": angulo, - "severidade": min(75, max(30, int(erro_ang * 3))), - "classe": "processo", - "descricao": f"Ângulo do bico {label} distante do desejado", - "acoes": [ - "Verificar servo/posição mecânica do bico.", - "Conferir neutro e ângulo de abertura configurados.", - ], - }) - score = min(score, 78) + divergencia_ligar = ( + comando and not estado_on + ) - return int(max(0, min(100, round(score)))), { + divergencia_desligar = ( + not comando and estado_on + ) + + divergencia_angulo = ( + usa_servo + and angulo is not None + and angulo_desejado is not None + and erro_angulo > self.ANGULO_TOL_BICO_GRAUS + ) + + # Durante estes contextos, o último comando ON pode estar + # temporariamente antigo em relação ao bloqueio operacional. + ignorar_divergencia_ligar = ( + ctx.get("calibrando", False) + or ctx.get("finalizando", False) + or ctx.get("pausa", False) + or ctx.get("emergencia", False) + ) + + # Durante calibragem, podem existir pulsos físicos controlados + # por uma sequência assíncrona própria. + ignorar_divergencia_desligar = ( + ctx.get("calibrando", False) + ) + + ignorar_divergencia_angulo = ( + ctx.get("calibrando", False) + or ctx.get("finalizando", False) + or ctx.get("emergencia", False) + ) + + # O ângulo só é avaliado quando o estado ON/OFF já está coerente. + avaliar_angulo = ( + divergencia_angulo + and not divergencia_ligar + and not divergencia_desligar + and not ignorar_divergencia_angulo + ) + + # ============================================================ + # Persistência das divergências + # ============================================================ + + tempo_divergencia_ligar = self._atualizar_persistencia( + runtime, + "comandado_on_leitura_off_desde", + ( + divergencia_ligar + and not ignorar_divergencia_ligar + ), + agora, + ) + + tempo_divergencia_desligar = self._atualizar_persistencia( + runtime, + "comandado_off_leitura_on_desde", + ( + divergencia_desligar + and not ignorar_divergencia_desligar + ), + agora, + ) + + tempo_divergencia_angulo = self._atualizar_persistencia( + runtime, + "angulo_divergente_desde", + avaliar_angulo, + agora, + ) + + debug = { "comando_disponivel": True, - "comando_atuar": comando, + "comando_fisico": comando, + "comando_logico": self._comando_logico_ligado(dados), "estado_on": estado_on, "usa_servo": usa_servo, "angulo": angulo, "angulo_desejado": angulo_desejado, "angulo_neutro": angulo_neutro, "angulo_controle": angulo_controle, + "erro_angulo": erro_angulo, "ultimo_comando_ms": ultimo_comando_ms, + "ultima_resposta_ms": ultima_resposta_ms, + "tem_comando_fisico": tem_comando_fisico, + "telemetria_fresca": telemetria_fresca, + "telemetria_pos_comando": telemetria_pos_comando, + "divergencia_ligar": divergencia_ligar, + "divergencia_desligar": divergencia_desligar, + "divergencia_angulo": divergencia_angulo, + "tempo_divergencia_ligar_s": tempo_divergencia_ligar, + "tempo_divergencia_desligar_s": tempo_divergencia_desligar, + "tempo_divergencia_angulo_s": tempo_divergencia_angulo, } + + # ============================================================ + # Contextos nos quais o comando ON não deve ser avaliado + # ============================================================ + + if divergencia_ligar and ignorar_divergencia_ligar: + debug["avaliacao_suspensa"] = ( + "contexto operacional bloqueia comando ON" + ) + + return 96, debug + + if ( + divergencia_desligar + and ignorar_divergencia_desligar + ): + debug["avaliacao_suspensa"] = ( + "bico em calibragem" + ) + + return 96, debug + + # ============================================================ + # Aguarda telemetria posterior ao comando + # ============================================================ + + possui_divergencia = ( + divergencia_ligar + or divergencia_desligar + or avaliar_angulo + ) + + if ( + possui_divergencia + and tem_comando_fisico + and not telemetria_pos_comando + ): + debug["aguardando_telemetria_pos_comando"] = True + + return 94, debug + + score = 100.0 + + alerta_comando_s = ( + self.COMANDO_ALERTA_MS / 1000.0 + ) + + falha_comando_s = ( + self.COMANDO_FALHA_MS / 1000.0 + ) + + # ============================================================ + # Comando ON, mas leitura permanece OFF + # ============================================================ + + if divergencia_ligar: + if tempo_divergencia_ligar < alerta_comando_s: + debug["divergencia_em_acomodacao"] = True + debug["tipo_divergencia"] = ( + "comandado_on_leitura_off" + ) + + return 96, debug + + falha_confirmada = ( + tempo_divergencia_ligar >= falha_comando_s + and telemetria_pos_comando + ) + + severidade = ( + 92 if falha_confirmada else 70 + ) + + motivos.append( + f"Bico comandado ligado, mas leitura " + f"ainda indica desligado ({label})" + ) + + condicoes.append({ + "label": "Bico", + "valor": 0, + "severidade": severidade, + "classe": ( + "falha_hardware" + if falha_confirmada + else "processo" + ), + "descricao": ( + f"Bico {label} permaneceu desligado " + "apesar do comando físico ON" + ), + "acoes": [ + "Verificar solenoide do bico.", + "Verificar alimentação e retorno do bico.", + "Conferir se o comando chegou ao ATU.", + ], + }) + + score = min( + score, + 45 if falha_confirmada else 75, + ) + + # ============================================================ + # Comando OFF, mas leitura permanece ON + # ============================================================ + + if divergencia_desligar: + if tempo_divergencia_desligar < 2.5: + debug["divergencia_em_acomodacao"] = True + debug["tipo_divergencia"] = ( + "comandado_off_leitura_on" + ) + + return 96, debug + + falha_confirmada = ( + tempo_divergencia_desligar >= 5.0 + and telemetria_pos_comando + ) + + severidade = ( + 95 if falha_confirmada else 82 + ) + + motivos.append( + f"Bico ligado sem comando ({label})" + ) + + condicoes.append({ + "label": "Bico", + "valor": 1, + "severidade": severidade, + "classe": ( + "falha_hardware" + if falha_confirmada + else "processo" + ), + "descricao": ( + f"Bico {label} permaneceu fisicamente ligado " + "apesar do comando OFF" + ), + "acoes": [ + "Verificar travamento da solenoide.", + "Verificar se o comando de desligamento chegou ao ATU.", + "Conferir a saída física da firmware.", + ], + }) + + score = min( + score, + 25 if falha_confirmada else 65, + ) + + # ============================================================ + # Estado correto, mas ângulo permanece divergente + # ============================================================ + + if avaliar_angulo: + if tempo_divergencia_angulo < alerta_comando_s: + debug["angulo_em_acomodacao"] = True + + return 96, debug + + falha_confirmada = ( + tempo_divergencia_angulo >= falha_comando_s + and telemetria_pos_comando + ) + + severidade = ( + 88 + if falha_confirmada + else min( + 75, + max( + 30, + int(erro_angulo * 3), + ), + ) + ) + + motivos.append( + f"Ângulo do bico distante do desejado " + f"({angulo:.1f}°/{angulo_desejado:.1f}°)" + ) + + condicoes.append({ + "label": "Ângulo bico", + "valor": angulo, + "severidade": severidade, + "classe": ( + "falha_hardware" + if falha_confirmada + else "processo" + ), + "descricao": ( + f"Ângulo do bico {label} permaneceu " + "distante do desejado" + ), + "acoes": [ + "Verificar servo e posição mecânica do bico.", + "Conferir neutro e ângulo de abertura.", + "Verificar telemetria angular do bico.", + ], + }) + + score = min( + score, + 45 if falha_confirmada else 78, + ) + + return int( + max( + 0, + min( + 100, + round(score), + ), + ) + ), debug # ============================================================ # Bombas @@ -1369,6 +1733,22 @@ class ModuloAtuador(ModuloDiagnosticoBase): return self.runtime_bombas[key] + def _runtime_bico(self, item): + key = str( + item.get("key") + or item.get("id") + or item.get("dados", {}).get("label") + or "bico" + ) + + if key not in self.runtime_bicos: + self.runtime_bicos[key] = { + "comandado_on_leitura_off_desde": None, + "comandado_off_leitura_on_desde": None, + } + + return self.runtime_bicos[key] + @staticmethod def _atualizar_persistencia(runtime, campo, condicao, agora): """ diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/sensoriamento.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/sensoriamento.py index af9f8f6aa..529854db9 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/sensoriamento.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/sensoriamento.py @@ -1,4 +1,5 @@ import time +import math from shared.enums import T_Code, StatusModulo from shared.utils import penalizar_frequencia from health_worker.modulos.base import ModuloDiagnosticoBase @@ -56,10 +57,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): operacao = ContextoGlobalRedis.get_operacao() or {} equipamento = ContextoGlobalRedis.get_equipamento() or {} - conectado = self._bool(dados_sen.get("conectado", False)) + conectado = self._bool(dados_sen.get("telemetria_recente", dados_sen.get("conectado", False))) + firmware_conectado = self._bool(dados_sen.get("firmware_conectado", conectado)) + root_score, root_motivos, root_cond = self._avaliar_root(dados_sen) - sen_disponivel = (conectado and self._ultima_resposta_ms(dados_sen) <= self.RESPOSTA_RECENTE_MS) + sen_disponivel = (conectado and firmware_conectado and self._ultima_resposta_ms(dados_sen) <= self.RESPOSTA_RECENTE_MS) sensores = self._coletar(dados_sen, "sensores") servos = self._coletar(dados_sen, "servos") @@ -78,7 +81,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): updates = {} for item in sensores: - s = self._avaliar_sensor(item, ctx) + s = self._avaliar_sensor(item, ctx, modulo_disponivel=sen_disponivel) saude_individual.append(s) updates[f"sensores.{item['key']}.saude"] = s @@ -126,6 +129,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): if not conectado: saude_final = 0 + elif not firmware_conectado: + saude_final = min(saude_final, 75) if any(self._is_falha_hardware(c) for c in root_cond + processo["condicoes_operacionais"]): saude_final = min(saude_final, 40) if processo.get("energia_critica", False): @@ -156,6 +161,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): saude_geral = { "conectado": conectado, + "telemetria_recente": conectado, + "firmware_conectado": firmware_conectado, "status": status.value, "saude": saude_final, "motivos": self._dedupe(motivos), @@ -196,7 +203,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): # ============================================================ def _avaliar_root(self, dados_sen): - conectado = self._bool(dados_sen.get("conectado", False)) + conectado = self._bool( + dados_sen.get( + "telemetria_recente", + dados_sen.get("conectado", False), + ) + ) idade_ms = self._ultima_resposta_ms(dados_sen) motivos = [] condicoes = [] @@ -275,22 +287,97 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): # Sensores passivos # ============================================================ - def _avaliar_sensor(self, item, ctx): + def _avaliar_sensor(self, item, ctx, modulo_disponivel=True): dados = item["dados"] conectado = self._bool(dados.get("conectado", False)) aferir = self._bool(dados.get("aferir", False)) mandatorio = self._bool(dados.get("mandatorio", False)) delegado_ao_mov = str(dados.get("label", "")).upper().startswith("TMV") em_uso = aferir and not delegado_ao_mov + + agora = time.monotonic() + runtime = self._runtime_saida_binaria(item) + + if not modulo_disponivel: + runtime["modulo_disponivel_anterior"] = False + runtime["componente_disponivel_anterior"] = False + return self._item( + item, StatusModulo.DESCONECTADO, 0, [], [], False, + { + "avaliacao_suspensa": True, + "motivo_suspensao": "Módulo SEN indisponível", + }, + "sensor", + ) + + if not runtime["modulo_disponivel_anterior"]: + runtime["modulo_recuperado_em"] = agora + runtime["componente_disponivel_anterior"] = False + runtime["modulo_disponivel_anterior"] = True + + tempo_retorno_modulo_ms = ( + agora - runtime["modulo_recuperado_em"] + ) * 1000.0 + + if not conectado: + runtime["componente_disponivel_anterior"] = False + + if tempo_retorno_modulo_ms < self.MODULO_RECUPERACAO_GRACE_MS: + return self._item( + item, StatusModulo.DESCONECTADO, 0, [], [], False, + { + "avaliacao_suspensa": True, + "motivo_suspensao": "Aguardando sensor após retorno do SEN", + "tempo_desde_retorno_modulo_ms": tempo_retorno_modulo_ms, + }, + "sensor", + ) + + return self._item( + item, + StatusModulo.DESCONECTADO, + 0, + ["desconectado após período de recuperação"], + [], + em_uso, + {"tempo_desde_retorno_modulo_ms": tempo_retorno_modulo_ms}, + "sensor", + ) + + if not runtime["componente_disponivel_anterior"]: + runtime["componente_recuperado_em"] = agora + runtime["componente_disponivel_anterior"] = True + + tempo_retorno_componente_ms = ( + agora - runtime["componente_recuperado_em"] + ) * 1000.0 + + if tempo_retorno_componente_ms < self.COMPONENTE_RECUPERACAO_GRACE_MS: + return self._item( + item, + StatusModulo.OPERANTE, + 100, + [], + [], + em_uso, + { + "avaliacao_suspensa": True, + "motivo_suspensao": "Estabilização após reconexão do sensor", + "tempo_desde_retorno_componente_ms": tempo_retorno_componente_ms, + "avaliacao_delegada_ao_mov": delegado_ao_mov, + }, + "sensor", + ) + motivos = [] condicoes = [] - if not conectado: - return self._item(item, StatusModulo.DESCONECTADO, 0, ["desconectado"], [], em_uso, {}, "sensor") - resposta_score = self._score_resposta(dados, motivos, condicoes, "Sensor SEN") - valor_score, valor_debug = self._avaliar_valores_sensor(item, ctx, motivos, condicoes) - telemetria = self._avaliar_telemetria(dados, motivos, self._labels_criticos_sensor(dados, ctx, mandatorio)) + criticos = self._labels_criticos_sensor(dados, ctx, mandatorio) + valor_score, valor_debug = self._avaliar_valores_sensor( + item, ctx, motivos, condicoes, criticos + ) + telemetria = self._avaliar_telemetria(dados, motivos, criticos) saude = resposta_score * 0.35 + valor_score * 0.45 + telemetria["score"] * 0.20 if any(self._is_falha_hardware(c) for c in condicoes): @@ -298,29 +385,62 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): saude = int(max(0, min(100, round(saude)))) status = self._resolver_status_item(conectado, saude, condicoes) - debug = { - "resposta": resposta_score, - "valores": valor_debug, - "telemetria": telemetria["parametros"], - "avaliacao_delegada_ao_mov": delegado_ao_mov, - } - return self._item(item, status, saude, motivos, condicoes, em_uso, debug, "sensor") - def _avaliar_valores_sensor(self, item, ctx, motivos, condicoes): + return self._item( + item, + status, + saude, + motivos, + condicoes, + em_uso, + { + "resposta": resposta_score, + "valores": valor_debug, + "telemetria": telemetria["parametros"], + "avaliacao_delegada_ao_mov": delegado_ao_mov, + }, + "sensor", + ) + + def _avaliar_valores_sensor(self, item, ctx, motivos, condicoes, criticos): dados = item["dados"] sensor_label = str(dados.get("label", "")) + criticos = {self._canon(c) for c in (criticos or []) if c} score = 100.0 debug = {} for key, campo in self._campos(dados).items(): label = str(campo.get("label") or key) - valor = self._float(campo.get("atual", 0), 0) + valor = self._float(campo.get("atual"), None) minimo = campo.get("minimo", None) maximo = campo.get("maximo", None) nominal = campo.get("nominal", None) unidade = campo.get("unidade", campo.get("unidade_medida", "")) debug[key] = {"label": label, "atual": valor, "minimo": minimo, "maximo": maximo, "nominal": nominal, "unidade": unidade} + + if valor is None: + critico = self._canon(label) in criticos + motivos.append(f"{sensor_label}.{label} possui leitura inválida") + condicoes.append({ + "label": "Leitura sensor SEN", + "valor": None, + "severidade": 92 if critico else 75, + "classe": "falha_hardware" if critico else "processo", + "descricao": f"{sensor_label}.{label} retornou valor inválido", + "acoes": [ + "Verificar sensor, alimentação e cabeamento.", + "Verificar sentinela e conversão da firmware.", + ], + }) + score = min(score, 0 if critico else 60) + debug[key] = { + "label": label, + "atual": None, + "valido": False, + } + continue + sc, conds, mots = self._avaliar_campo(sensor_label, label, valor, minimo, maximo) score = min(score, sc) motivos.extend(mots) @@ -509,6 +629,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): token_desejado = round(angulo_sp, 3) self._registrar_desejado(runtime, token_desejado) + self._atualizar_evidencia_comando(runtime, evidencia) + + idade_desejo_ms = self._idade_desejo_ms(runtime) + debug["idade_desejo_ms"] = idade_desejo_ms + debug["telemetria_confirmada_desejo"] = runtime["telemetria_confirmada_desejo"] erro = abs(angulo - angulo_sp) debug["erro"] = erro @@ -532,12 +657,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): debug["avaliacao_suspensa"] = "aguardando primeiro comando" return 96, debug - if not evidencia["telemetria_pos_comando"]: + if not runtime["telemetria_confirmada_desejo"]: runtime["divergencia_desde"] = None debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando" return 96, debug - if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS: + if idade_desejo_ms < self.COMANDO_GRACE_MS: runtime["divergencia_desde"] = None debug["avaliacao_suspensa"] = "período de resposta ao comando" return 96, debug @@ -862,6 +987,9 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): "coerencia_desde": None, "nivel_divergencia": None, "ultimo_desejado": None, + "desejo_alterado_em": None, + "telemetria_confirmada_desejo": False, + "possui_comando_anterior": False, }) def _preparar_atuador_auxiliar( @@ -995,10 +1123,18 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): def _registrar_desejado(self, runtime, token_desejado): if runtime.get("ultimo_desejado") == token_desejado: - return + return False + + self._resetar_estado_comando( + runtime, + manter_desejado=False, + ) - self._resetar_estado_comando(runtime, manter_desejado=False) runtime["ultimo_desejado"] = token_desejado + runtime["desejo_alterado_em"] = time.monotonic() + runtime["telemetria_confirmada_desejo"] = False + runtime["possui_comando_anterior"] = False + return True @staticmethod def _resetar_estado_comando(runtime, manter_desejado=True): @@ -1007,6 +1143,43 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): runtime["nivel_divergencia"] = None if not manter_desejado: runtime["ultimo_desejado"] = None + runtime["desejo_alterado_em"] = None + runtime["telemetria_confirmada_desejo"] = False + runtime["possui_comando_anterior"] = False + + @staticmethod + def _atualizar_evidencia_comando(runtime, evidencia): + possui_comando = bool( + evidencia.get("possui_comando", False) + ) + + if not possui_comando: + runtime["possui_comando_anterior"] = False + runtime["telemetria_confirmada_desejo"] = False + return + + if not runtime.get("possui_comando_anterior", False): + runtime["possui_comando_anterior"] = True + runtime["desejo_alterado_em"] = time.monotonic() + runtime["telemetria_confirmada_desejo"] = False + runtime["divergencia_desde"] = None + runtime["coerencia_desde"] = None + runtime["nivel_divergencia"] = None + + if evidencia.get("telemetria_pos_comando", False): + runtime["telemetria_confirmada_desejo"] = True + + @staticmethod + def _idade_desejo_ms(runtime): + alterado_em = runtime.get("desejo_alterado_em") + + if alterado_em is None: + return 999999.0 + + return max( + 0.0, + (time.monotonic() - alterado_em) * 1000.0, + ) def _avaliar_comando_binario( self, @@ -1019,10 +1192,7 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): ): dados = item["dados"] label = dados.get("label") - - controlar = self._bool( - dados.get("controlar", False) - ) + controlar = self._bool(dados.get("controlar", False)) desejado_presente = any( chave in dados @@ -1051,89 +1221,95 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): lido = self._estado_ligado( dados.get( "estado_leitura", - self._campo_atual( - dados, - "EstadoLeitura", - False, - ), + self._campo_atual(dados, "EstadoLeitura", False), ) ) - ultimo_comando_ms = self._float( - dados.get("ultimo_comando_ms", 999999), - 999999, - ) - - ultima_resposta_ms = self._float( - dados.get("ultima_resposta_ms", 999999), - 999999, - ) - - possui_comando = self._bool( - dados.get( - "possui_comando", - ultimo_comando_ms < 900000, - ) - ) - - if "telemetria_pos_comando" in dados: - telemetria_pos_comando = self._bool( - dados.get("telemetria_pos_comando", False) - ) - else: - # Compatibilidade com publicação antiga. - telemetria_pos_comando = ( - possui_comando and - ultima_resposta_ms <= ultimo_comando_ms + 100.0 - ) + evidencia = self._evidencia_comando(dados) debug = { "controlar": controlar, "estado_lido": lido, "estado_desejado": desejado, - "possui_comando": possui_comando, - "telemetria_pos_comando": telemetria_pos_comando, - "ultimo_comando_ms": ultimo_comando_ms, - "ultima_resposta_ms": ultima_resposta_ms, + **evidencia, } if not controlar: - runtime["divergencia_desde"] = None + self._resetar_estado_comando(runtime, manter_desejado=False) return 100, debug if not desejado_presente or not leitura_presente: - runtime["divergencia_desde"] = None + self._resetar_estado_comando(runtime, manter_desejado=False) debug["contrato_comando_completo"] = False return 92, debug - # Estado coerente: limpa qualquer divergência anterior. + token_desejado = bool(desejado) + self._registrar_desejado(runtime, token_desejado) + self._atualizar_evidencia_comando(runtime, evidencia) + + idade_desejo_ms = self._idade_desejo_ms(runtime) + debug["idade_desejo_ms"] = idade_desejo_ms + debug["telemetria_confirmada_desejo"] = runtime["telemetria_confirmada_desejo"] + if lido == desejado: runtime["divergencia_desde"] = None debug["divergente"] = False - return 100, debug + + if runtime["nivel_divergencia"] is None: + runtime["coerencia_desde"] = None + return 100, debug + + agora = time.monotonic() + if runtime["coerencia_desde"] is None: + runtime["coerencia_desde"] = agora + + tempo_coerente_ms = ( + agora - runtime["coerencia_desde"] + ) * 1000.0 + + debug["recuperacao_em_confirmacao"] = True + debug["tempo_coerente_ms"] = tempo_coerente_ms + + if tempo_coerente_ms >= self.COMANDO_RECUPERACAO_MS: + runtime["nivel_divergencia"] = None + runtime["coerencia_desde"] = None + return 100, debug + + motivos.append( + f"{label} recuperou o estado desejado; confirmando estabilidade" + ) + condicoes.append({ + "label": nome, + "valor": int(lido), + "severidade": 55, + "classe": "processo", + "descricao": ( + f"{label} voltou ao estado desejado e está em confirmação" + ), + "acoes": [ + "Aguardar confirmação da estabilidade do retorno.", + ], + }) + return 78, debug debug["divergente"] = True + runtime["coerencia_desde"] = None - # Não existe evidência de que o estado desejado foi enviado. - if not possui_comando: + if not evidencia["possui_comando"]: runtime["divergencia_desde"] = None debug["avaliacao_suspensa"] = "aguardando primeiro comando" return 96, debug - # A resposta ainda é antiga em relação ao comando. - if not telemetria_pos_comando: + if not runtime["telemetria_confirmada_desejo"]: runtime["divergencia_desde"] = None debug["avaliacao_suspensa"] = ( "aguardando telemetria posterior ao comando" ) return 96, debug - # Comando acabou de ser enviado. - if ultimo_comando_ms < self.COMANDO_GRACE_MS: + if idade_desejo_ms < self.COMANDO_GRACE_MS: runtime["divergencia_desde"] = None - debug["avaliacao_suspensa"] = ( - "período de resposta ao comando" - ) + debug["avaliacao_suspensa"] = "período de resposta ao comando" return 96, debug agora = time.monotonic() @@ -1147,16 +1323,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): < self.COMPONENTE_RECUPERACAO_GRACE_MS ): runtime["divergencia_desde"] = None - debug["avaliacao_suspensa"] = ( - "estabilização após reconexão" - ) + debug["avaliacao_suspensa"] = "estabilização após reconexão" debug["tempo_desde_retorno_componente_ms"] = ( tempo_desde_retorno_componente_ms ) return 96, debug - # A persistência começa quando a divergência é efetivamente - # observada em condições válidas. if runtime["divergencia_desde"] is None: runtime["divergencia_desde"] = agora @@ -1166,39 +1338,54 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): debug["tempo_divergente_ms"] = tempo_divergente_ms + if runtime["nivel_divergencia"] == "falha": + motivos.append(f"{label} não acompanhou o estado desejado") + condicoes.append({ + "label": nome, + "valor": int(lido), + "severidade": severidade_falha, + "classe": "falha_hardware", + "descricao": f"{label} permaneceu diferente do estado desejado", + "acoes": [ + "Verificar saída física do relé.", + "Verificar carga conectada.", + "Verificar comando e retorno do SEN.", + ], + }) + return 30, debug + if tempo_divergente_ms < self.COMANDO_ALERTA_MS: return 96, debug if tempo_divergente_ms < self.COMANDO_FALHA_MS: + runtime["nivel_divergencia"] = "alerta" + motivos.append( f"{label} ainda não acompanhou o estado desejado" ) - condicoes.append({ "label": nome, "valor": int(lido), "severidade": 78, "classe": "processo", - "descricao": - f"{label} apresenta divergência persistente de comando", + "descricao": ( + f"{label} apresenta divergência persistente de comando" + ), "acoes": [ "Aguardar confirmação ou verificar saída física.", ], }) - return 68, debug - motivos.append( - f"{label} não acompanhou o estado desejado" - ) + runtime["nivel_divergencia"] = "falha" + motivos.append(f"{label} não acompanhou o estado desejado") condicoes.append({ "label": nome, "valor": int(lido), "severidade": severidade_falha, "classe": "falha_hardware", - "descricao": - f"{label} permaneceu diferente do estado desejado", + "descricao": f"{label} permaneceu diferente do estado desejado", "acoes": [ "Verificar saída física do relé.", "Verificar carga conectada.", @@ -1319,6 +1506,11 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): st_sp if status_sp_presente else None, ) self._registrar_desejado(runtime, token_desejado) + self._atualizar_evidencia_comando(runtime, evidencia) + + idade_desejo_ms = self._idade_desejo_ms(runtime) + debug["idade_desejo_ms"] = idade_desejo_ms + debug["telemetria_confirmada_desejo"] = runtime["telemetria_confirmada_desejo"] # Se o SP existe, uma leitura ausente (-1) também é divergência. coerente_id = ( @@ -1366,12 +1558,12 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): debug["avaliacao_suspensa"] = "aguardando primeiro comando" return 96, debug - if not evidencia["telemetria_pos_comando"]: + if not runtime["telemetria_confirmada_desejo"]: runtime["divergencia_desde"] = None debug["avaliacao_suspensa"] = "aguardando telemetria posterior ao comando" return 96, debug - if evidencia["ultimo_comando_ms"] < self.COMANDO_GRACE_MS: + if idade_desejo_ms < self.COMANDO_GRACE_MS: runtime["divergencia_desde"] = None debug["avaliacao_suspensa"] = "período de resposta ao comando" return 96, debug @@ -1653,6 +1845,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): if not conectado: return StatusModulo.DESCONECTADO em_uso = [x for x in saude_individual if self._bool(x.get("em_uso", False))] + if any(x.get("status") == StatusModulo.DESCONECTADO.value and self._bool(x.get("mandatorio", False)) for x in em_uso): + return StatusModulo.FALHA if any(self._is_falha_hardware(c) for c in condicoes): return StatusModulo.FALHA if any(x.get("status") == StatusModulo.FALHA.value for x in em_uso): @@ -1665,6 +1859,8 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): return StatusModulo.ALERTA if any(x.get("status") == StatusModulo.ALERTA.value for x in em_uso): return StatusModulo.ALERTA + if any(x.get("status") == StatusModulo.DESCONECTADO.value for x in em_uso): + return StatusModulo.ALERTA return StatusModulo.OPERANTE @staticmethod @@ -1795,14 +1991,24 @@ class ModuloSensoriamento(ModuloDiagnosticoBase): @staticmethod def _float(valor, padrao=0.0): + fallback = None if padrao is None else float(padrao) + try: if valor is None: - return float(padrao) + return fallback + if hasattr(valor, "value"): valor = valor.value - return float(valor) + + numero = float(valor) + + if not math.isfinite(numero): + return fallback + + return numero + except Exception: - return float(padrao) + return fallback @staticmethod def _safe_int(valor, padrao=0): diff --git a/Firmware/Modulos/CanService.h b/Firmware/Modulos/CanService.h index 244dda335..cd28724e0 100644 --- a/Firmware/Modulos/CanService.h +++ b/Firmware/Modulos/CanService.h @@ -78,6 +78,31 @@ public: // Mantem o watchdog de tasks como ultimo cinto de seguranca contra firmware // realmente travado. Ele nao e usado para decidir se o barramento esta vivo. esp_task_wdt_init(TASK_WDT_SECONDS, true); + + // PARA VERSAO 3.3.11 do CORE + /*esp_task_wdt_config_t wdtConfig = {}; + + wdtConfig.timeout_ms = static_cast(TASK_WDT_SECONDS) * 1000UL; + + // Nao inscrevemos automaticamente as idle tasks. + // Nosso CanService registra explicitamente apenas as tasks + // que deseja supervisionar. + wdtConfig.idle_core_mask = 0; + + wdtConfig.trigger_panic = true; + + esp_err_t wdtErr = esp_task_wdt_reconfigure(&wdtConfig); + + // Se ainda nao tiver sido inicializado pelo core, + // inicializa agora. + if (wdtErr == ESP_ERR_INVALID_STATE) { + wdtErr = esp_task_wdt_init(&wdtConfig); + } + + if (wdtErr != ESP_OK) { + PrintTela("[CAN] Falha ao configurar Task WDT: " + String((int)wdtErr)); + }*/ + esp_task_wdt_add(CanTaskRxHandle); esp_task_wdt_add(CanTaskTxHandle); esp_task_wdt_add(CallbackWorkerHandle); diff --git a/Firmware/Modulos/I2CService.h b/Firmware/Modulos/I2CService.h index 7f44ac353..5f67bff6d 100644 --- a/Firmware/Modulos/I2CService.h +++ b/Firmware/Modulos/I2CService.h @@ -76,6 +76,11 @@ class I2CService { uint32_t readyDropsAds = 0; uint32_t falhasIoAds = 0; uint32_t wireTimeoutMs = 0; + + // Recovery local: nao altera a geracao do Wire. + uint32_t recoveriesLocaisMux = 0; + uint32_t recoveriesLocaisAds = 0; + uint32_t falhasLocaisDispositivos = 0; }; static DiagnosticoCAN ObterDiagnosticoCAN() { @@ -121,6 +126,10 @@ class I2CService { d.readyDropsAds = _readyDropsAds; d.falhasIoAds = _falhasIoAds; + d.recoveriesLocaisMux = _recoveriesLocaisMux; + d.recoveriesLocaisAds = _recoveriesLocaisAds; + d.falhasLocaisDispositivos = _falhasLocaisDispositivos; + portEXIT_CRITICAL(&_estadoLock); d.infraestruturaMuxOk = (d.muxEsperadoMask & d.muxValidadoMask) == d.muxEsperadoMask; @@ -390,8 +399,58 @@ class I2CService { return RecuperarBarramento(motivo, ignorarCooldown); } + /* + * Falha de um dispositivo individual NAO e falha global do barramento. + * + * O proprio model do sensor continua responsavel por: + * - contar falhas consecutivas; + * - marcar Iniciado=false; + * - tentar Inicializar() novamente. + * + * O I2CService apenas registra a degradacao local. Isso evita que um + * unico sensor derrube a geracao de todos os outros. + */ static void NotificarFalhaDispositivo(int idSensor, const String& motivo) { - RegistrarFalhaTransacao("ID " + String(idSensor) + " | " + motivo); + RegistrarFalhaLocalDispositivo(idSensor, motivo); + } + + static void NotificarSucessoDispositivo(int idSensor) { + RegistrarSucessoLocalDispositivo(idSensor); + } + + /* + * Use SOMENTE depois que o model do sensor esgotar suas tentativas locais + * de reinicializacao. Esta e a ponte explicita entre recovery local e uma + * nova geracao global. + * + * Nao chamar por amostra invalida isolada. + */ + static void NotificarFalhaPersistenteDispositivo( + int idSensor, + const String& motivo + ) { + RegistrarFalhaLocalDispositivo( + idSensor, + "persistente | " + motivo + ); + + AgendarRecovery( + "Dispositivo nao reviveu apos retries locais | ID " + + String(idSensor) + " | " + motivo + ); + } + + /* + * Por padrao mantemos o ultimo canal do MUX selecionado entre acessos. + * No SEN isso e seguro porque os INAs downstream usam 0x40 e os + * dispositivos do barramento principal usam outros enderecos. + * + * Isso reduz praticamente pela metade as escritas no TCA9548A. + * Pode ser reativado para outras topologias que precisem isolar o ramal + * ao final de cada acesso. + */ + static void DefinirDesabilitarMuxAoLiberar(bool ativo) { + _desabilitarMuxAoLiberar = ativo; } static bool CanalMuxEmQuarentena(uint8_t canalGlobal) { @@ -578,6 +637,9 @@ class I2CService { s += " SCL=" + String((_pinoSCL >= 0) ? digitalRead(_pinoSCL) : -1); s += " q_mux=" + String(QuantidadeCanaisMuxEmQuarentena()); s += " q_ads=" + String(QuantidadeADSEmQuarentena()); + s += " rec_local_mux=" + String(_recoveriesLocaisMux); + s += " rec_local_ads=" + String(_recoveriesLocaisAds); + s += " falhas_locais=" + String(_falhasLocaisDispositivos); if (_pinoTcaRdy >= 0) { s += " tca_rdy=" + String(digitalRead(_pinoTcaRdy) == HIGH ? 1 : 0); @@ -761,6 +823,7 @@ class I2CService { ContextoBarramento contexto = _contextoAtual; if ( + _desabilitarMuxAoLiberar && contexto.valido && contexto.canalMuxSolicitado >= 0 && !RecoveryPendente() @@ -944,65 +1007,240 @@ class I2CService { return TrocarCanalMux(enderecoMux + indiceMux, canal); } - static bool TrocarCanalMux(uint8_t endereco, uint8_t canalMux) { - int indiceMux = IndiceMux(endereco); + static bool EscreverMascaraMuxSemRegistrar( + uint8_t endereco, + uint8_t mascara, + uint8_t& erro, + uint32_t& duracaoMs + ) { + const uint32_t inicio = millis(); - if (indiceMux < 0 || canalMux >= 8) { - MostrarLog( - "MUX", - "Endereco ou canal invalido | endereco=0x" + - String(endereco, HEX) + " canal=" + String(canalMux) - ); + Wire.beginTransmission(endereco); + Wire.write(mascara); + erro = Wire.endTransmission(); + + duracaoMs = + (uint32_t)(millis() - inicio); + + return erro == 0; + } + + /* + * Recovery local do TCA9548A. + * + * Reseta SOMENTE o MUX, revalida o endereco e volta a selecionar o + * canal desejado. Nao desmonta Wire e nao altera a geracao. + */ + static bool RecuperarMuxLocalSemGeracao( + uint8_t endereco, + uint8_t canalMux + ) { + const int indiceMux = + IndiceMux(endereco); + + if ( + indiceMux < 0 || + canalMux >= 8 + ) { return false; } portENTER_CRITICAL(&_estadoLock); - _muxEsperadoMask |= (uint8_t)(1U << indiceMux); + if (_recoveriesLocaisMux < UINT32_MAX) { + _recoveriesLocaisMux++; + } portEXIT_CRITICAL(&_estadoLock); - uint8_t canalGlobal = (indiceMux * 10) + canalMux; - - int canalAnterior = CanalGlobalAtivoAtual(); - int sdaAntes = digitalRead(_pinoSDA); - int sclAntes = digitalRead(_pinoSCL); - int rdyAntes = TCAReadyOk() ? 1 : 0; - uint32_t inicio = millis(); + MostrarLog( + "MUX", + "Recovery LOCAL | endereco=0x" + + String(endereco, HEX) + + " canal=" + String(canalMux) + + " | geracao=" + + String(GeracaoBarramento()) + ); - if (CanalMuxEmQuarentena(canalGlobal)) { - MostrarLog("MUX", "Canal bloqueado por quarentena | canalGlobal=" + String(canalGlobal)); + if (!ResetarMuxSePossivel()) { return false; } - if (!_muxValidado[indiceMux]) { - MostrarLog("MUX", "Validando MUX em 0x" + String(endereco, HEX)); + if (!TCAReadyOk()) { + return false; + } - if (!VerificaEnderecoBarramento(endereco)) { - MostrarLog("MUX", "MUX nao encontrado em 0x" + String(endereco, HEX)); - return false; + if ( + !PingEnderecoSemRegistrar( + endereco, + 80 + ) + ) { + return false; + } + + _muxValidado[indiceMux] = true; + MuxIniciado = true; + + uint8_t erro = 0xFF; + uint32_t duracao = 0; + + const bool ok = + EscreverMascaraMuxSemRegistrar( + endereco, + (uint8_t)(1U << canalMux), + erro, + duracao + ); + + if (!ok) { + MostrarLog( + "MUX", + "Recovery LOCAL nao conseguiu selecionar canal" + " | erro=" + String(erro) + + " duracao=" + String(duracao) + "ms" + ); + + return false; + } + + enderecoMuxAtivo = endereco; + canalAtivo = canalMux; + + MostrarLog( + "MUX", + "Recovery LOCAL concluido" + " | canal=" + String(canalMux) + + " | geracao=" + + String(GeracaoBarramento()) + ); + + return true; + } + + static bool TrocarCanalMux( + uint8_t endereco, + uint8_t canalMux + ) { + const int indiceMux = + IndiceMux(endereco); + + if ( + indiceMux < 0 || + canalMux >= 8 + ) { + MostrarLog( + "MUX", + "Endereco ou canal invalido | endereco=0x" + + String(endereco, HEX) + + " canal=" + + String(canalMux) + ); + + return false; + } + + portENTER_CRITICAL(&_estadoLock); + _muxEsperadoMask |= + (uint8_t)(1U << indiceMux); + portEXIT_CRITICAL(&_estadoLock); + + const uint8_t canalGlobal = + (uint8_t)( + (indiceMux * 10) + + canalMux + ); + + if ( + CanalMuxEmQuarentena( + canalGlobal + ) + ) { + MostrarLog( + "MUX", + "Canal bloqueado por quarentena" + " | canalGlobal=" + + String(canalGlobal) + ); + + return false; + } + + /* + * Se o MUX ainda nao foi validado, primeiro tentamos uma descoberta + * passiva. Ausencia momentanea NAO soma falha global. + */ + if (!_muxValidado[indiceMux]) { + const bool encontrado = + PingEnderecoSemRegistrar( + endereco, + 80 + ); + + if (!encontrado) { + if ( + !RecuperarMuxLocalSemGeracao( + endereco, + canalMux + ) + ) { + RegistrarFalhaTransacao( + "MUX nao respondeu apos recovery local" + ); + + return false; + } + + RegistrarSucessoTransacao(); + return true; } _muxValidado[indiceMux] = true; MuxIniciado = true; } - if (enderecoMuxAtivo == endereco && canalAtivo == canalMux) { + if ( + enderecoMuxAtivo == endereco && + canalAtivo == canalMux + ) { return true; } - // Evita deixar dois ramais conectados simultaneamente. - if (enderecoMuxAtivo != 0xFF && enderecoMuxAtivo != endereco) { - Wire.beginTransmission(enderecoMuxAtivo); - Wire.write(0x00); - int erroDesabilitar = Wire.endTransmission(); + /* + * SEN atual possui um unico TCA9548A. Este bloco preserva suporte a + * multiplos MUXes: se um endereco diferente ficou selecionado, + * desabilita-o antes de ativar o proximo. + */ + if ( + enderecoMuxAtivo != 0xFF && + enderecoMuxAtivo != endereco + ) { + uint8_t erroDesabilitar = 0xFF; + uint32_t duracaoDesabilitar = 0; - if (erroDesabilitar != 0) { + bool desabilitou = + EscreverMascaraMuxSemRegistrar( + enderecoMuxAtivo, + 0x00, + erroDesabilitar, + duracaoDesabilitar + ); + + if (!desabilitou) { MostrarLog( "MUX", - "Falha ao desabilitar MUX anterior 0x" + + "Falha ao desabilitar MUX anterior" + " | endereco=0x" + String(enderecoMuxAtivo, HEX) + - " | erro=" + String(erroDesabilitar) + " erro=" + + String(erroDesabilitar) + + " duracao=" + + String(duracaoDesabilitar) + + "ms" ); - RegistrarFalhaTransacao("Falha ao desabilitar MUX anterior"); + + RegistrarFalhaTransacao( + "Falha ao desabilitar MUX anterior" + ); + return false; } @@ -1010,51 +1248,84 @@ class I2CService { canalAtivo = -1; } - Wire.beginTransmission(endereco); - Wire.write((uint8_t)(1U << canalMux)); - int erro = Wire.endTransmission(); + const int canalAnterior = + CanalGlobalAtivoAtual(); - uint32_t duracao = (uint32_t)(millis() - inicio); - int sdaDepois = digitalRead(_pinoSDA); - int sclDepois = digitalRead(_pinoSCL); - int rdyDepois = TCAReadyOk() ? 1 : 0; + uint8_t ultimoErro = 0xFF; + uint32_t ultimaDuracao = 0; - MostrarLog( - "MUX", - "Troca" - " | anterior=" + String(canalAnterior) + - " solicitado=" + String(canalGlobal) + - " erro=" + String(erro) + - " duracao=" + String(duracao) + "ms" + - " antes[SDA=" + String(sdaAntes) + - " SCL=" + String(sclAntes) + - " RDY=" + String(rdyAntes) + "]" + - " depois[SDA=" + String(sdaDepois) + - " SCL=" + String(sclDepois) + - " RDY=" + String(rdyDepois) + "]" - ); + for ( + uint8_t tentativa = 0; + tentativa < + TentativasTrocaMuxAntesResetLocal; + tentativa++ + ) { + const bool ok = + EscreverMascaraMuxSemRegistrar( + endereco, + (uint8_t)(1U << canalMux), + ultimoErro, + ultimaDuracao + ); + + if (ok) { + enderecoMuxAtivo = endereco; + canalAtivo = canalMux; + + RegistrarSucessoTransacao(); + + return true; + } - if (erro != 0) { MostrarLog( "MUX", - "Falha ao trocar canal | endereco=0x" + String(endereco, HEX) + - " canal=" + String(canalMux) + " erro=" + String(erro) + "Falha local ao selecionar canal" + " | anterior=" + + String(canalAnterior) + + " solicitado=" + + String(canalGlobal) + + " tentativa=" + + String(tentativa + 1) + + " erro=" + + String(ultimoErro) + + " duracao=" + + String(ultimaDuracao) + + "ms" + " SDA=" + + String(digitalRead(_pinoSDA)) + + " SCL=" + + String(digitalRead(_pinoSCL)) + + " RDY=" + + String(TCAReadyOk() ? 1 : 0) ); - RegistrarFalhaTransacao("Falha ao trocar canal MUX"); - return false; + + DelayMs(2); } - enderecoMuxAtivo = endereco; - canalAtivo = canalMux; - RegistrarSucessoTransacao(); + /* + * As tentativas simples falharam. Antes de tocar no Wire, recupera + * SOMENTE o MUX e tenta novamente. + */ + if ( + RecuperarMuxLocalSemGeracao( + endereco, + canalMux + ) + ) { + RegistrarSucessoTransacao(); + return true; + } - MostrarLog( - "MUX", - "Canal ativo | endereco=0x" + String(endereco, HEX) + - " canal=" + String(canalMux) + /* + * Somente agora uma falha do MUX alimenta o contador GLOBAL. + * Sao necessarias LimiteFalhasTransacaoRecovery ocorrencias + * consecutivas deste nivel para gerar uma nova geracao. + */ + RegistrarFalhaTransacao( + "MUX nao reviveu apos retries e reset local" ); - return true; + return false; } static int SelecionarCanalADS(uint8_t canalGlobal) { @@ -1106,6 +1377,11 @@ class I2CService { const bool leituraOk = LerADSSingleShotComTimeout(canal, leituraADC); if (!leituraOk) { + TratarFalhaADSLocal( + idSensor, + "falha de transporte/conversao" + ); + leituraFalhou = true; break; } @@ -1127,6 +1403,14 @@ class I2CService { leiturasConcluidas++; } + if ( + !leituraFalhou && + leiturasConcluidas == leituras + ) { + ResetarFalhasAdsLocais(); + RegistrarSucessoLocalDispositivo(idSensor); + } + if (RecoveryPendente()) { leituraFalhou = true; } @@ -1323,9 +1607,10 @@ class I2CService { static ContextoBarramento _contextoFalha; static portMUX_TYPE _estadoLock; - // Transacoes deste SEN sao de poucos bytes a 100 kHz. - // 25 ms ainda e uma margem muito grande, mas limita melhor uma - // transferencia realmente emperrada. + // No core 2.0.11 existem caminhos do Wire que podem ignorar este + // limite e retornar apenas ~1 s depois. Mantemos 25 ms como intencao do + // driver, mas o recovery local evita transformar esse stall em reboot + // global imediato. static constexpr uint32_t TimeoutWireMs = 25; static constexpr uint32_t LimiteReadyLowMs = 200; static constexpr uint32_t CooldownRecoveryMs = 1000; @@ -1350,6 +1635,24 @@ class I2CService { static uint32_t _readyDropsAds; static uint32_t _falhasIoAds; + // Saude/recovery local. Nenhum destes contadores muda a geracao. + static uint32_t _recoveriesLocaisMux; + static uint32_t _recoveriesLocaisAds; + static uint32_t _falhasLocaisDispositivos; + + static uint8_t _falhasLocaisPorId[256]; + + static uint8_t _falhasAdsConsecutivas; + static uint8_t _falhasRecoveryLocalAdsConsecutivas; + static uint32_t _ultimoRecoveryLocalAdsMs; + + static bool _desabilitarMuxAoLiberar; + + static constexpr uint8_t LimiteFalhasAdsAntesRecoveryLocal = 3; + static constexpr uint8_t LimiteFalhasRecoveryLocalAdsAntesGlobal = 2; + static constexpr uint32_t CooldownRecoveryLocalAdsMs = 500; + static constexpr uint8_t TentativasTrocaMuxAntesResetLocal = 2; + static void MostrarLog(const String& componente, const String& mensagem) { if (DebugMode) { PrintTela("[" + componente + "] " + mensagem); @@ -1975,6 +2278,195 @@ class I2CService { return total; } + static void RegistrarFalhaLocalDispositivo( + int idSensor, + const String& motivo + ) { + uint8_t consecutivas = 0; + + portENTER_CRITICAL(&_estadoLock); + + if (_falhasLocaisDispositivos < UINT32_MAX) { + _falhasLocaisDispositivos++; + } + + if (idSensor >= 0 && idSensor <= 255) { + uint8_t& valor = + _falhasLocaisPorId[(uint8_t)idSensor]; + + if (valor < 255) { + valor++; + } + + consecutivas = valor; + } + + portEXIT_CRITICAL(&_estadoLock); + + MostrarLog( + "I2C/LOCAL", + "ID " + String(idSensor) + + " | falha local #" + String(consecutivas) + + " | " + motivo + ); + } + + static void RegistrarSucessoLocalDispositivo(int idSensor) { + if (idSensor < 0 || idSensor > 255) { + return; + } + + portENTER_CRITICAL(&_estadoLock); + _falhasLocaisPorId[(uint8_t)idSensor] = 0; + portEXIT_CRITICAL(&_estadoLock); + } + + static void ResetarFalhasAdsLocais() { + portENTER_CRITICAL(&_estadoLock); + _falhasAdsConsecutivas = 0; + _falhasRecoveryLocalAdsConsecutivas = 0; + portEXIT_CRITICAL(&_estadoLock); + } + + static void RegistrarFalhaAdsConsecutiva() { + portENTER_CRITICAL(&_estadoLock); + + if (_falhasAdsConsecutivas < 255) { + _falhasAdsConsecutivas++; + } + + portEXIT_CRITICAL(&_estadoLock); + } + + static uint8_t FalhasAdsConsecutivas() { + uint8_t valor; + + portENTER_CRITICAL(&_estadoLock); + valor = _falhasAdsConsecutivas; + portEXIT_CRITICAL(&_estadoLock); + + return valor; + } + + /* + * Recovery local do ADS: + * ADS power-cycle -> ping -> begin -> gain/rate + * + * Nao toca em Wire.end()/Wire.begin(), logo NAO altera a geracao e + * nao invalida INAs/MUX/outros sensores. + * + * Esta funcao e chamada com ownership do i2cMutex. + */ + static bool RecuperarADSLocalSemGeracao() { + const uint32_t agora = millis(); + + if ( + _ultimoRecoveryLocalAdsMs > 0 && + !TempoPassou( + _ultimoRecoveryLocalAdsMs, + CooldownRecoveryLocalAdsMs + ) + ) { + return false; + } + + _ultimoRecoveryLocalAdsMs = agora; + + portENTER_CRITICAL(&_estadoLock); + if (_recoveriesLocaisAds < UINT32_MAX) { + _recoveriesLocaisAds++; + } + portEXIT_CRITICAL(&_estadoLock); + + MostrarLog( + "ADS", + "Recovery LOCAL iniciado | sem alterar geracao" + ); + + AdsIniciado = false; + ultimoEnderecoADS = 0xFF; + + if (_pinoAdsPwrEn >= 0) { + ResetarADSSePossivel(); + } + else { + DelayMs(2); + } + + const bool pingOk = + PingEnderecoSemRegistrar( + enderecoAds, + 80 + ); + + const bool iniciou = + pingOk && + IniciarADSSemMutex( + enderecoAds + ); + + if (iniciou) { + ResetarFalhasAdsLocais(); + + MostrarLog( + "ADS", + "Recovery LOCAL concluido | geracao=" + + String(GeracaoBarramento()) + ); + + return true; + } + + uint8_t falhasRecovery; + + portENTER_CRITICAL(&_estadoLock); + + if ( + _falhasRecoveryLocalAdsConsecutivas < + 255 + ) { + _falhasRecoveryLocalAdsConsecutivas++; + } + + falhasRecovery = + _falhasRecoveryLocalAdsConsecutivas; + + portEXIT_CRITICAL(&_estadoLock); + + MostrarLog( + "ADS", + "Recovery LOCAL falhou | tentativas=" + + String(falhasRecovery) + ); + + if ( + falhasRecovery >= + LimiteFalhasRecoveryLocalAdsAntesGlobal + ) { + AgendarRecovery( + "ADS nao reviveu apos recoveries locais" + ); + } + + return false; + } + + static void TratarFalhaADSLocal(int idSensor, const String& motivo) { + RegistrarFalhaLocalDispositivo( + idSensor, + "ADS | " + motivo + ); + + RegistrarFalhaAdsConsecutiva(); + + if ( + FalhasAdsConsecutivas() >= + LimiteFalhasAdsAntesRecoveryLocal + ) { + (void)RecuperarADSLocalSemGeracao(); + } + } + static void RegistrarFalhaTransacao(const String& motivo) { uint32_t falhas; @@ -2455,7 +2947,13 @@ class I2CService { static void RegistrarTimeoutADS(uint32_t inicioMs, const char* motivo) { AtualizarMaxDuracaoADS(inicioMs); IncrementarTimeoutADS(); - RegistrarFalhaTransacao(motivo); + + // Timeout isolado do ADS e degradacao LOCAL. + MostrarLog( + "ADS", + String(motivo) + + " | tratado como falha local" + ); } static void RegistrarReadyDropADS(uint32_t inicioMs) { @@ -2469,6 +2967,7 @@ class I2CService { portEXIT_CRITICAL(&_estadoLock); + // READY pertence a infraestrutura global do barramento. RegistrarFalhaTransacao("READY caiu durante leitura ADS"); } @@ -2481,10 +2980,14 @@ class I2CService { AtualizarMaxDuracaoADS(inicioMs); IncrementarFalhaIoADS(); - RegistrarFalhaTransacao( - String("ADS ") + etapa + + // Erro de I/O do ADS continua local. O chamador decide quando + // tentar power-cycle do ADS, sem alterar a geracao. + MostrarLog( + "ADS", + String(etapa) + " | erro=" + String(erro) + - " | duracao=" + String(duracaoMs) + "ms" + " | duracao=" + + String(duracaoMs) + "ms" ); } @@ -2518,7 +3021,10 @@ class I2CService { endereco == 0xFF || IndiceADS(endereco) < 0 ) { - RegistrarFalhaTransacao("ADS sem endereco ativo"); + MostrarLog( + "ADS", + "Sem endereco ativo para leitura" + ); return false; } @@ -2769,4 +3275,16 @@ uint32_t I2CService::_timeoutsAds = 0; uint32_t I2CService::_readyDropsAds = 0; uint32_t I2CService::_falhasIoAds = 0; +uint32_t I2CService::_recoveriesLocaisMux = 0; +uint32_t I2CService::_recoveriesLocaisAds = 0; +uint32_t I2CService::_falhasLocaisDispositivos = 0; + +uint8_t I2CService::_falhasLocaisPorId[256] = {0}; + +uint8_t I2CService::_falhasAdsConsecutivas = 0; +uint8_t I2CService::_falhasRecoveryLocalAdsConsecutivas = 0; +uint32_t I2CService::_ultimoRecoveryLocalAdsMs = 0; + +bool I2CService::_desabilitarMuxAoLiberar = false; + #endif diff --git a/Firmware/Modulos/Pinout.h b/Firmware/Modulos/Pinout.h index 10a8b57f8..ae5266656 100644 --- a/Firmware/Modulos/Pinout.h +++ b/Firmware/Modulos/Pinout.h @@ -108,6 +108,9 @@ class PinoModel { if (barramento == CONTROLADOR) { if (tipo == _PWM) { ledcDetachPin(num); + + // PARA VERSAO 3.3.11 do core + //ledcDetach(static_cast(num)); } pinMode(num, INPUT); } diff --git a/Firmware/Modulos/SensorCorrenteModel_v2.h b/Firmware/Modulos/SensorCorrenteModel_v2.h index cd86049e9..f3af76bba 100644 --- a/Firmware/Modulos/SensorCorrenteModel_v2.h +++ b/Firmware/Modulos/SensorCorrenteModel_v2.h @@ -48,11 +48,19 @@ class SensorCorrente_v2 : public ComponenteCAN { } bool EstaIniciado() { + const uint32_t geracaoAtual = I2CService::GeracaoBarramento(); + bool iniciado; + uint32_t geracaoInicializacao; + portENTER_CRITICAL(&_snapshotLock); iniciado = Iniciado; + geracaoInicializacao = _geracaoI2CInicializacao; portEXIT_CRITICAL(&_snapshotLock); - return iniciado; + + return iniciado && + geracaoAtual != 0 && + geracaoInicializacao == geracaoAtual; } uint8_t FalhasConsecutivas() { @@ -84,7 +92,7 @@ class SensorCorrente_v2 : public ComponenteCAN { isINA219 = (_A_Shunt == TiposShuntCorrente::INA219_3A2); - bool encontrado = I2CService::VerificaEnderecoBarramento(_Endereco); + bool encontrado = I2CService::VerificaEnderecoBarramentoPassivo(_Endereco); if (!encontrado) { AtualizarEstadoIniciado(false); MostrarLog("Endereco I2C nao encontrado: " + String(_Endereco)); @@ -94,7 +102,7 @@ class SensorCorrente_v2 : public ComponenteCAN { if (isINA219) { sINA219 = Adafruit_INA219(_Endereco); - bool iniciou = sINA219.begin(); + bool iniciou = sINA219.begin(&Wire); AtualizarEstadoIniciado(iniciou); if (iniciou) { @@ -106,7 +114,7 @@ class SensorCorrente_v2 : public ComponenteCAN { } else { sINA228 = Adafruit_INA228(); - bool iniciou = sINA228.begin(_Endereco); + bool iniciou = sINA228.begin(_Endereco, &Wire); AtualizarEstadoIniciado(iniciou); if (iniciou) { @@ -148,6 +156,7 @@ class SensorCorrente_v2 : public ComponenteCAN { data.push_back(static_cast(posicao)); data.push_back(ID_Num); + const uint32_t geracaoAtual = I2CService::GeracaoBarramento(); bool iniciado; bool ina219; float snapBusVoltage; @@ -159,7 +168,9 @@ class SensorCorrente_v2 : public ComponenteCAN { // Copia curta e atômica. Serialização acontece fora da critical section. portENTER_CRITICAL(&_snapshotLock); - iniciado = Iniciado; + iniciado = Iniciado && + geracaoAtual != 0 && + _geracaoI2CInicializacao == geracaoAtual; ina219 = isINA219; snapBusVoltage = busVoltage; snapCurrent = current; @@ -210,11 +221,16 @@ class SensorCorrente_v2 : public ComponenteCAN { portMUX_TYPE _snapshotLock = portMUX_INITIALIZER_UNLOCKED; uint8_t _falhasConsecutivas = 0; uint32_t _totalFalhasLeitura = 0; + uint32_t _geracaoI2CInicializacao = 0; static constexpr uint8_t LimiteFalhasAntesReinit = 3; void AtualizarEstadoIniciado(bool iniciado) { + const uint32_t geracao = + iniciado ? I2CService::GeracaoBarramento() : 0; + portENTER_CRITICAL(&_snapshotLock); Iniciado = iniciado; + _geracaoI2CInicializacao = geracao; portEXIT_CRITICAL(&_snapshotLock); } @@ -238,12 +254,16 @@ class SensorCorrente_v2 : public ComponenteCAN { AtualizarEstadoIniciado(false); MostrarLog("Sensor marcado para reinicializacao controlada"); } + + I2CService::NotificarFalhaDispositivo(ID_Num, motivo); } void RegistrarSucessoAquisicao() { portENTER_CRITICAL(&_snapshotLock); _falhasConsecutivas = 0; portEXIT_CRITICAL(&_snapshotLock); + + I2CService::NotificarSucessoDispositivo(ID_Num); } bool AmostraValida( @@ -307,7 +327,7 @@ class SensorCorrente_v2 : public ComponenteCAN { // As APIs Adafruit abaixo nao propagam de forma util os erros de Wire. // Este ping confirma o dispositivo no ramal antes da aquisicao. - if (!I2CService::VerificaEnderecoBarramento(_Endereco)) { + if (!I2CService::VerificaEnderecoBarramentoPassivo(_Endereco)) { I2CService::LiberarAcessoI2C(ID_Num); RegistrarFalhaAquisicao("dispositivo nao respondeu no endereco I2C"); return; @@ -341,12 +361,6 @@ class SensorCorrente_v2 : public ComponenteCAN { ); if (!amostraValida) { - // Ainda dentro do ownership do I2C: o service consegue associar a - // falha ao ID/canal correto antes de o mutex ser liberado. - I2CService::NotificarFalhaDispositivo( - ID_Num, - "amostra INA invalida" - ); I2CService::LiberarAcessoI2C(ID_Num); RegistrarFalhaAquisicao("amostra invalida/fora da faixa"); return; diff --git a/Firmware/Modulos/SensorTemperaturaNtcModel.h b/Firmware/Modulos/SensorTemperaturaNtcModel.h index 935af89c0..724844724 100644 --- a/Firmware/Modulos/SensorTemperaturaNtcModel.h +++ b/Firmware/Modulos/SensorTemperaturaNtcModel.h @@ -2,28 +2,97 @@ #include "Pinout.h" #include "Utils.h" #include "IComponenteCAN.h" -#include +#include "I2CService.h" + +#include +#include +#include +#include #ifndef SensorTemperaturaNtcModel #define SensorTemperaturaNtcModel +/* + * SensorTemperaturaNtc - versão produto + * + * Topologia física da placa: + * + * Retorno do motor ---- Rserie 4k7 ----+---- ADC / ADS1115 + * | + * Rgnd 6k8 + * | + * GND + * + * Logo, visto pelo retorno do motor, a placa adiciona uma carga: + * + * Rcarga = Rserie + Rgnd = 11k5 + * + * e a tensão realmente lida pelo ADC é: + * + * Vadc = Vretorno * Rgnd / (Rserie + Rgnd) + * + * A classe reconstrói Vretorno antes de calcular o NTC. + * + * + * ntcGnd = false: + * + * 5 V + * | + * NTC + * | + * Vretorno ------- Rserie ------- ADC + * | | + * Rfixo 10k Rgnd + * | | + * GND GND + * + * + * ntcGnd = true: + * + * 5 V + * | + * Rfixo 10k + * | + * Vretorno ------- Rserie ------- ADC + * | | + * NTC Rgnd + * | | + * GND GND + * + * + * Para EXPANSOR_ADS: + * - usa geração do Wire/I2C; + * - faz uma única aquisição lógica por ciclo; + * - preserva a última amostra válida em falhas; + * - invalida o lifecycle após falhas consecutivas; + * - o retry externo pode reinicializar o sensor. + */ class SensorTemperaturaNtc : public ComponenteCAN { public: String ID; uint8_t ID_Num = 0; PinoModel _pino; - bool _ntcGnd = false; - - int leitura = 0; - double temperatura = -1; - double tensao = 0; + // Mantido público para compatibilidade com código legado. bool Iniciado = false; + int leitura = -1; + double temperatura = NAN; + + // Tensão REAL medida no pino do ADC/ADS, depois do divisor 4k7/6k8. + double tensao = NAN; + float OffsetTemperatura = 0.0f; SensorTemperaturaNtc() {} + /* + * Mantém exatamente o contrato histórico do construtor. + * + * ntcGnd: + * false = NTC no lado de 5 V + * true = NTC no lado de GND + */ SensorTemperaturaNtc( const String& id, uint8_t idNum, @@ -35,91 +104,312 @@ class SensorTemperaturaNtc : public ComponenteCAN { ) : ID(id), ID_Num(idNum), - _pino(PinoModel(pino, tipoBarramento, _INPUT, ANALOGICO, idNum, canalMux)), + _pino( + PinoModel( + pino, + tipoBarramento, + _INPUT, + ANALOGICO, + idNum, + canalMux + ) + ), OffsetTemperatura(offsetTemperatura), - _ntcGnd(ntcGnd) + _ntcGnd(ntcGnd), + _tipoBarramento(tipoBarramento) { } + bool EstaIniciado() { + bool iniciado; + uint32_t geracaoInicializacao; + + portENTER_CRITICAL(&_snapshotLock); + iniciado = Iniciado; + geracaoInicializacao = _geracaoI2CInicializacao; + portEXIT_CRITICAL(&_snapshotLock); + + if (!iniciado) { + return false; + } + + // ADC interno não depende do lifecycle do Wire. + if (_tipoBarramento != TiposBarramentos::EXPANSOR_ADS) { + return true; + } + + const uint32_t geracaoAtual = + I2CService::GeracaoBarramento(); + + return + I2CService::I2CIniciado && + I2CService::AdsIniciado && + geracaoAtual != 0 && + geracaoInicializacao == geracaoAtual; + } + + bool PossuiAmostraValida() { + bool valor; + + portENTER_CRITICAL(&_snapshotLock); + valor = _possuiAmostraValida; + portEXIT_CRITICAL(&_snapshotLock); + + return valor; + } + + uint8_t FalhasConsecutivas() { + uint8_t valor; + + portENTER_CRITICAL(&_snapshotLock); + valor = _falhasConsecutivas; + portEXIT_CRITICAL(&_snapshotLock); + + return valor; + } + + uint32_t TotalFalhasLeitura() { + uint32_t valor; + + portENTER_CRITICAL(&_snapshotLock); + valor = _totalFalhasLeitura; + portEXIT_CRITICAL(&_snapshotLock); + + return valor; + } + + uint32_t IdadeUltimaAmostraValidaMs() { + uint32_t ultima; + bool possui; + + portENTER_CRITICAL(&_snapshotLock); + ultima = _ultimaAmostraValidaMs; + possui = _possuiAmostraValida; + portEXIT_CRITICAL(&_snapshotLock); + + if (!possui || ultima == 0) { + return UINT32_MAX; + } + + return (uint32_t)(millis() - ultima); + } + void Inicializar() { - if (Iniciado) { + if (EstaIniciado()) { MostrarLog("Sensor ja inicializado"); return; } - leitura = 0; - temperatura = -1; - tensao = 0; + // Limpa lifecycle, mas preserva a última leitura válida. + AtualizarEstadoIniciado(false); + + if (_tipoBarramento == TiposBarramentos::EXPANSOR_ADS) { + if (!I2CService::I2CIniciado) { + MostrarLog( + "Sensor nao iniciado | I2C indisponivel" + ); + return; + } + + if (!I2CService::AdsIniciado) { + MostrarLog( + "Sensor nao iniciado | ADS1115 indisponivel" + ); + return; + } + } _pino.Conectar(); - _pino.ConfigurarLeituraAnalogica(12, ADC_11db); - Iniciado = _pino.Definido(); - - MostrarLog(Iniciado ? "Sensor iniciado" : "Sensor nao iniciado"); - } - - void Desligar() { - if (!Iniciado) { - MostrarLog("Sensor nao esta inicializado"); + if (!_pino.Definido() || !_pino.Conectado) { + MostrarLog( + "Sensor nao iniciado | pino/barramento indisponivel" + ); return; } - _pino.Desconectar(); + /* + * Se algum dia esta classe voltar ao ADC interno, + * preserva a configuração histórica de 12 bits / 11 dB. + * + * Para o ADS1115 isso não é usado. + */ + if (_tipoBarramento == TiposBarramentos::CONTROLADOR) { + analogReadResolution(12); + analogSetPinAttenuation( + _pino.num, + ADC_11db + ); + } - Iniciado = false; + AtualizarEstadoIniciado(true); + + portENTER_CRITICAL(&_snapshotLock); + _falhasConsecutivas = 0; + portEXIT_CRITICAL(&_snapshotLock); + + MostrarLog( + "Sensor iniciado" + " | canal=" + String(_pino.num) + + " | ntc_gnd=" + String(_ntcGnd ? 1 : 0) + + " | geracao=" + + String(_geracaoI2CInicializacao) + ); + } + + void Desligar() { + _pino.Desconectar(); + AtualizarEstadoIniciado(false); MostrarLog("Sensor desligado"); } void RequisitarDados() { - if (!Iniciado) return; + if (!EstaIniciado()) { + /* + * Caso a geração tenha mudado, força Iniciado=false + * para que o retry periódico da firmware enxergue + * corretamente a necessidade de reinicialização. + */ + bool estavaMarcadoIniciado; + + portENTER_CRITICAL(&_snapshotLock); + estavaMarcadoIniciado = Iniciado; + portEXIT_CRITICAL(&_snapshotLock); + + if ( + estavaMarcadoIniciado && + _tipoBarramento == + TiposBarramentos::EXPANSOR_ADS + ) { + AtualizarEstadoIniciado(false); + } + + return; + } + AferirTemperatura(); } - std::vector MontarMensagemCAN(CanMessagePosicaoDados posicao) { + std::vector MontarMensagemCAN( + CanMessagePosicaoDados posicao + ) { std::vector data; - data.push_back(static_cast(posicao)); + + data.push_back( + static_cast(posicao) + ); data.push_back(ID_Num); + const bool iniciado = + EstaIniciado(); + int snapLeitura; double snapTemperatura; double snapTensao; + bool snapValido; + portENTER_CRITICAL(&_snapshotLock); snapLeitura = leitura; snapTemperatura = temperatura; snapTensao = tensao; + snapValido = _possuiAmostraValida; portEXIT_CRITICAL(&_snapshotLock); switch (posicao) { case CanMessagePosicaoDados::Status: { - data.push_back(Iniciado ? 1 : 0); + data.push_back( + iniciado ? 1 : 0 + ); break; } case CanMessagePosicaoDados::Dados1: { + /* + * Mantém o contrato CAN atual: + * + * [2..3] raw ADC normalizado 0..4095 + * [4..5] temperatura * 100 + * [6..7] tensão no PINO ADC em mV + */ + + uint16_t leituraAdc = + snapValido && + snapLeitura >= 0 + ? static_cast( + snapLeitura + ) + : 0xFFFF; + int16_t temperaturaInt = + snapValido && isfinite(snapTemperatura) - ? static_cast(round(snapTemperatura * 100.0f)) + ? static_cast( + constrain( + (long)lround( + snapTemperatura * + 100.0 + ), + (long)INT16_MIN + 1, + (long)INT16_MAX + ) + ) : INT16_MIN; uint16_t tensaoMv = - isfinite(snapTensao) && snapTensao >= 0.0 - ? static_cast(round(snapTensao * 1000.0)) + snapValido && + isfinite(snapTensao) && + snapTensao >= 0.0 + ? static_cast( + constrain( + (long)lround( + snapTensao * + 1000.0 + ), + 0L, + 65534L + ) + ) : 0xFFFF; - uint16_t leituraAdc = static_cast(snapLeitura); + data.push_back( + static_cast( + leituraAdc >> 8 + ) + ); + data.push_back( + static_cast( + leituraAdc & 0xFF + ) + ); - data.push_back(static_cast(leituraAdc >> 8)); - data.push_back(static_cast(leituraAdc & 0xFF)); + data.push_back( + static_cast( + (uint16_t)temperaturaInt >> 8 + ) + ); + data.push_back( + static_cast( + (uint16_t)temperaturaInt & 0xFF + ) + ); - data.push_back(static_cast(temperaturaInt >> 8)); - data.push_back(static_cast(temperaturaInt & 0xFF)); + data.push_back( + static_cast( + tensaoMv >> 8 + ) + ); + data.push_back( + static_cast( + tensaoMv & 0xFF + ) + ); - data.push_back(static_cast(tensaoMv >> 8)); - data.push_back(static_cast(tensaoMv & 0xFF)); break; } + + default: + break; } return data; @@ -127,176 +417,675 @@ class SensorTemperaturaNtc : public ComponenteCAN { private: bool DebugMode = false; - portMUX_TYPE _snapshotLock = portMUX_INITIALIZER_UNLOCKED; - // SimpleKalmanFilter filtro = SimpleKalmanFilter(5, 15, 0.01); - void MostrarLog(String mensagem) { + /* + * false: + * 5V -> NTC -> retorno -> Rfixo -> GND + * + * true: + * 5V -> Rfixo -> retorno -> NTC -> GND + */ + bool _ntcGnd = false; + + TiposBarramentos _tipoBarramento = + TiposBarramentos::CONTROLADOR; + + portMUX_TYPE _snapshotLock = + portMUX_INITIALIZER_UNLOCKED; + + uint8_t _falhasConsecutivas = 0; + uint32_t _totalFalhasLeitura = 0; + + uint32_t _geracaoI2CInicializacao = 0; + + bool _possuiAmostraValida = false; + uint32_t _ultimaAmostraValidaMs = 0; + + /* + * Hardware real. + */ + static constexpr float Vs = 5.0f; + + static constexpr float Rfixo = + 10000.0f; + + static constexpr float Rserie = + 4700.0f; + + static constexpr float Rgnd = + 6800.0f; + + /* + * Do ponto de vista do Vretorno: + * + * retorno -> 4k7 -> ADC -> 6k8 -> GND + * + * equivale a 11k5 para GND. + */ + static constexpr float Rcarga = + Rserie + Rgnd; + + /* + * ADS1115 atual: + * I2CService usa GAIN_ONE. + * + * readADC_SingleEnded() é convertido no service + * de 0..32767 para 0..4095. + * + * Portanto: + * + * 4095 <=> 4.096 V + */ + static constexpr float + FullScaleAdsV = 4.096f; + + /* + * O máximo físico no ADC devido ao divisor 4k7/6k8 é: + * + * 5V * 6k8 / (4k7 + 6k8) + * ~= 2.9565 V + */ + static constexpr float + TensaoAdcMaxFisicaV = + Vs * Rgnd / Rcarga; + + static constexpr float + MargemTensaoV = 0.010f; + + static constexpr float Beta = + 3950.0f; + + static constexpr float T0K = + 298.15f; + + static constexpr float Rntc25 = + 10000.0f; + + static constexpr float + TemperaturaMinC = -40.0f; + + static constexpr float + TemperaturaMaxC = 150.0f; + + /* + * ADS1115 já é delta-sigma. + * 4 amostras por NTC são suficientes para uma grandeza lenta. + * + * 4 NTC * 4 conversões * 2 Hz = 32 conversões/s. + */ + static constexpr int + QuantidadeAmostras = 4; + + /* + * Filtro final leve. + * O valor só entra aqui depois de passar por todas as validações. + */ + static constexpr float + AlphaTemperatura = 0.35f; + + static constexpr uint8_t + LimiteFalhasAntesReinit = 3; + + void MostrarLog( + const String& mensagem + ) { if (DebugMode) { - PrintTela("[TMP " + ID + "] " + mensagem); + PrintTela( + "[TMP " + + ID + + "] " + + mensagem + ); } } - void AferirTemperatura() { - constexpr int QuantidadeAmostras = 16; - - int novaLeitura = _pino.get(QuantidadeAmostras); - const int tensaoMv = _pino.get(QuantidadeAmostras, FormatoLeituraAnalogica::Milivolts); - - double novaTensao = NAN; - double novaTemperatura = NAN; - - if (novaLeitura < 0 || tensaoMv < 0) { - novaLeitura = -1; - - portENTER_CRITICAL(&_snapshotLock); - leitura = novaLeitura; - tensao = novaTensao; - temperatura = novaTemperatura; - portEXIT_CRITICAL(&_snapshotLock); - - MostrarLog("Falha na leitura do sensor NTC"); - return; - } - - novaTensao = tensaoMv / 1000.0f; - - // Detecta curto, pino flutuante, circuito aberto ou saturação. - if (novaLeitura <= 20 || novaLeitura >= 4080) { - portENTER_CRITICAL(&_snapshotLock); - leitura = novaLeitura; - tensao = novaTensao; - temperatura = novaTemperatura; - portEXIT_CRITICAL(&_snapshotLock); - - MostrarLog("Leitura NTC fora da faixa | Raw: " + String(novaLeitura) + " | Tensao: " + String(novaTensao, 3) + " V"); - return; - } - - if (novaLeitura > 20 && tensaoMv < 0) - novaTemperatura = CalcularTemperatura(static_cast(novaLeitura), OffsetTemperatura); - else - novaTemperatura = CalcularTemperaturaPorTensao(static_cast(tensaoMv), _ntcGnd, OffsetTemperatura); + void AtualizarEstadoIniciado( + bool iniciado + ) { + const uint32_t geracao = + iniciado && + _tipoBarramento == + TiposBarramentos::EXPANSOR_ADS + ? I2CService::GeracaoBarramento() + : 0; portENTER_CRITICAL(&_snapshotLock); - leitura = novaLeitura; - tensao = novaTensao; - temperatura = novaTemperatura; + + Iniciado = iniciado; + _geracaoI2CInicializacao = + geracao; + + portEXIT_CRITICAL(&_snapshotLock); + } + + void RegistrarFalhaAquisicao( + const String& motivo + ) { + uint8_t falhas; + + portENTER_CRITICAL(&_snapshotLock); + + if ( + _falhasConsecutivas < + UINT8_MAX + ) { + _falhasConsecutivas++; + } + + _totalFalhasLeitura++; + falhas = + _falhasConsecutivas; + portEXIT_CRITICAL(&_snapshotLock); MostrarLog( - "Leitura: " + String(novaLeitura) + - " | Tensao: " + String(novaTensao, 3) + " V" + - " | Temperatura: " + String(novaTemperatura, 2) + " C" + "Falha de leitura " + + String(falhas) + + "/" + + String( + LimiteFalhasAntesReinit + ) + + " | " + + motivo + ); + + /* + * Nunca sobrescreve o último valor bom com NaN. + * + * Se a falha persistir, o sensor perde o estado + * Iniciado e entra no retry controlado da firmware. + */ + if ( + falhas >= + LimiteFalhasAntesReinit + ) { + AtualizarEstadoIniciado(false); + + MostrarLog( + "Sensor marcado para " + "reinicializacao controlada" + ); + } + } + + void RegistrarSucessoAquisicao() { + portENTER_CRITICAL(&_snapshotLock); + _falhasConsecutivas = 0; + portEXIT_CRITICAL(&_snapshotLock); + } + + /* + * Converte o raw 0..4095 recebido do I2CService + * para a tensão REAL no pino do ADS1115. + */ + bool ConverterRawParaTensaoAdc( + int raw, + float& vAdc + ) { + if ( + raw < 0 || + raw > 4095 + ) { + return false; + } + + if ( + _tipoBarramento == + TiposBarramentos::EXPANSOR_ADS + ) { + vAdc = + ((float)raw / 4095.0f) * + FullScaleAdsV; + } + else { + /* + * Fallback histórico para ADC de 12 bits. + * Esta versão produto está otimizada para ADS. + * + * Se voltar ao ADC interno, recomenda-se usar + * analogReadMilliVolts() no PinoModel para calibração absoluta. + */ + vAdc = + ((float)raw / 4095.0f) * + 3.3f; + } + + if (!isfinite(vAdc)) { + return false; + } + + /* + * Com o divisor físico, o ADS jamais deveria receber + * mais que aproximadamente 2.9565 V. + * + * Deixamos pequena tolerância para resistores/medição. + */ + if ( + _tipoBarramento == + TiposBarramentos::EXPANSOR_ADS && + ( + vAdc < + -MargemTensaoV || + vAdc > + TensaoAdcMaxFisicaV + + 0.050f + ) + ) { + return false; + } + + return true; + } + + /* + * Reconstrói a tensão do nó original do sensor, + * antes do divisor 4k7/6k8. + * + * Vadc = + * Vretorno * + * Rgnd / + * (Rserie + Rgnd) + * + * logo: + * + * Vretorno = + * Vadc * + * (Rserie + Rgnd) / + * Rgnd + */ + bool CalcularTensaoRetorno( + float vAdc, + float& vRetorno + ) { + if ( + !isfinite(vAdc) || + vAdc < 0.0f + ) { + return false; + } + + vRetorno = + vAdc * + Rcarga / + Rgnd; + + if (!isfinite(vRetorno)) { + return false; + } + + if ( + vRetorno <= + MargemTensaoV || + vRetorno >= + Vs - + MargemTensaoV + ) { + return false; + } + + return true; + } + + /* + * Calcula Rntc considerando também a carga + * criada pelo divisor 4k7 + 6k8. + */ + bool CalcularResistenciaNtc( + float vRetorno, + float& resistenciaNtc + ) { + if ( + !isfinite(vRetorno) || + vRetorno <= + MargemTensaoV || + vRetorno >= + Vs - + MargemTensaoV + ) { + return false; + } + + if (_ntcGnd) { + /* + * 5V -> Rfixo -> retorno -> NTC -> GND + * + * Abaixo do retorno temos: + * + * NTC || Rcarga + * + * Primeiro calculamos a resistência equivalente + * inferior do divisor: + * + * Req = + * Rfixo * + * Vretorno / + * (Vs - Vretorno) + */ + const float rEquivalente = + Rfixo * + vRetorno / + (Vs - vRetorno); + + if ( + !isfinite(rEquivalente) || + rEquivalente <= 0.0f + ) { + return false; + } + + /* + * 1 / Req = + * 1 / Rntc + + * 1 / Rcarga + */ + const float inversoRntc = + (1.0f / rEquivalente) - + (1.0f / Rcarga); + + if ( + !isfinite(inversoRntc) || + inversoRntc <= 0.0f + ) { + return false; + } + + resistenciaNtc = + 1.0f / + inversoRntc; + } + else { + /* + * 5V -> NTC -> retorno -> Rfixo -> GND + * + * Abaixo do retorno temos: + * + * Rfixo || Rcarga + */ + const float rInferior = + (Rfixo * Rcarga) / + (Rfixo + Rcarga); + + if ( + !isfinite(rInferior) || + rInferior <= 0.0f + ) { + return false; + } + + /* + * Vretorno = + * + * Vs * + * rInferior / + * (Rntc + rInferior) + * + * logo: + * + * Rntc = + * rInferior * + * (Vs - Vretorno) / + * Vretorno + */ + resistenciaNtc = + rInferior * + (Vs - vRetorno) / + vRetorno; + } + + return + isfinite(resistenciaNtc) && + resistenciaNtc > 0.0f; + } + + bool CalcularTemperaturaC( + float resistenciaNtc, + float& temperaturaC + ) { + if ( + !isfinite(resistenciaNtc) || + resistenciaNtc <= 0.0f + ) { + return false; + } + + const float termoLog = + logf( + resistenciaNtc / + Rntc25 + ); + + if (!isfinite(termoLog)) { + return false; + } + + const float inversoT = + (1.0f / T0K) + + (termoLog / Beta); + + if ( + !isfinite(inversoT) || + inversoT <= 0.0f + ) { + return false; + } + + temperaturaC = + (1.0f / inversoT) - + 273.15f + + OffsetTemperatura; + + if ( + !isfinite(temperaturaC) || + temperaturaC < + TemperaturaMinC || + temperaturaC > + TemperaturaMaxC + ) { + return false; + } + + temperaturaC = + roundf( + temperaturaC * + 100.0f + ) / + 100.0f; + + return true; + } + + void PublicarAmostraValida( + int novaLeitura, + float novaTensaoAdc, + float novaTemperatura + ) { + float temperaturaPublicada = + novaTemperatura; + + portENTER_CRITICAL(&_snapshotLock); + + if ( + _possuiAmostraValida && + isfinite(temperatura) + ) { + temperaturaPublicada = + AlphaTemperatura * + novaTemperatura + + ( + 1.0f - + AlphaTemperatura + ) * + (float)temperatura; + } + + leitura = + novaLeitura; + + /* + * Mantemos no CAN a tensão que realmente + * chegou ao ADC, depois do divisor 4k7/6k8. + */ + tensao = + novaTensaoAdc; + + temperatura = + roundf( + temperaturaPublicada * + 100.0f + ) / + 100.0f; + + _possuiAmostraValida = + true; + + _ultimaAmostraValidaMs = + millis(); + + portEXIT_CRITICAL(&_snapshotLock); + } + + void AferirTemperatura() { + /* + * UMA única aquisição lógica. + * + * No EXPANSOR_ADS: + * PinoModel::get(N) -> + * I2CService::RealizarLeituraADS() + * + * O ownership do I2C fica mantido durante + * todas as N conversões. + */ + const int novaLeitura = + _pino.get( + QuantidadeAmostras + ); + + if (novaLeitura < 0) { + RegistrarFalhaAquisicao( + "falha ao obter amostra ADC" + ); + return; + } + + float novaTensaoAdc = NAN; + + if ( + !ConverterRawParaTensaoAdc( + novaLeitura, + novaTensaoAdc + ) + ) { + RegistrarFalhaAquisicao( + "raw/tensao ADC invalido" + " | raw=" + + String(novaLeitura) + ); + return; + } + + float vRetorno = NAN; + + if ( + !CalcularTensaoRetorno( + novaTensaoAdc, + vRetorno + ) + ) { + RegistrarFalhaAquisicao( + "tensao de retorno invalida" + " | raw=" + + String(novaLeitura) + + " | Vadc=" + + String( + novaTensaoAdc, + 3 + ) + ); + return; + } + + float resistenciaNtc = NAN; + + if ( + !CalcularResistenciaNtc( + vRetorno, + resistenciaNtc + ) + ) { + RegistrarFalhaAquisicao( + "resistencia NTC invalida" + " | Vadc=" + + String( + novaTensaoAdc, + 3 + ) + + " | Vret=" + + String( + vRetorno, + 3 + ) + ); + return; + } + + float novaTemperatura = NAN; + + if ( + !CalcularTemperaturaC( + resistenciaNtc, + novaTemperatura + ) + ) { + RegistrarFalhaAquisicao( + "temperatura fora da faixa" + " | Rntc=" + + String( + resistenciaNtc, + 1 + ) + + " | Vret=" + + String( + vRetorno, + 3 + ) + ); + return; + } + + RegistrarSucessoAquisicao(); + + PublicarAmostraValida( + novaLeitura, + novaTensaoAdc, + novaTemperatura + ); + + MostrarLog( + "Raw=" + + String(novaLeitura) + + " | Vadc=" + + String( + novaTensaoAdc, + 3 + ) + + " V | Vret=" + + String( + vRetorno, + 3 + ) + + " V | Rntc=" + + String( + resistenciaNtc, + 1 + ) + + " ohm | T=" + + String( + novaTemperatura, + 2 + ) + + " C" ); } - - float CalcularTemperatura(float leituraAdc, float offset = 0.0f) { - const float Vs = 5.0f; - const float Beta = 3950.0f; - const float To = 298.15f; - const float Ro = 10000.0f; - const float adcMax = 4095.0f; - static constexpr float divisor = 1.0f; - static constexpr float vOffset = (2.516f / 3.036f); - - if (leituraAdc < 200.0f) return -1.0f; - - float Vadc = leituraAdc / adcMax * Vs; - Vadc = Vadc * divisor * vOffset; - - tensao = Vadc; - - if (Vadc <= 0.001f) return -100.0f; - if (Vadc >= Vs) return NAN; - - float Rt = Ro * (Vadc / (Vs - Vadc)); - float T = 1.0f / (1.0f / To + log(Rt / Ro) / Beta); - float Tc = T - 273.15f + offset; - - return roundf(Tc * 100.0f) / 100.0f; - } - - float CalcularTemperaturaPorTensao(float tensaoAdcMv, bool ntcGnd, float offset = 0.0f) { - constexpr float Vs = 5.0f; - - constexpr float Rfixo = 10000.0f; // Resistor interno do motor - - // Valores reais da placa: - constexpr float Rserie = 4700.0f; // Retorno até o GPIO - constexpr float Rgnd = 6800.0f; // GPIO até o GND - constexpr float Rcarga = Rserie + Rgnd; - - constexpr float Beta = 3950.0f; - constexpr float T0 = 298.15f; // 25 °C em kelvin - constexpr float Rntc25 = 10000.0f; - - const float vAdc = tensaoAdcMv / 1000.0f; - - if (!isfinite(vAdc) || vAdc <= 0.01f || vAdc >= 3.05f) - return NAN; - - // Tensão no retorno do motor, antes do resistor de 4,7 kΩ. - const float vRetorno = vAdc * (Rserie + Rgnd) / Rgnd; - - if (!isfinite(vRetorno) || vRetorno <= 0.01f || vRetorno >= Vs - 0.01f) { - return NAN; - } - - float resistenciaNtc = NAN; - - if (ntcGnd) { - /* - 5 V - | - Rfixo - | - retorno - | - NTC - | - GND - A carga externa Rcarga fica em paralelo com o NTC. - */ - - const float rEquivalente = Rfixo * vRetorno / (Vs - vRetorno); - const float inversoRntc = (1.0f / rEquivalente) - (1.0f / Rcarga); - if (!isfinite(inversoRntc) || inversoRntc <= 0.0f) - return NAN; - resistenciaNtc = 1.0f / inversoRntc; - } - else { - /* - 5 V - | - NTC - | - retorno - | - Rfixo - | - GND - A carga externa Rcarga fica em paralelo com Rfixo. - */ - - const float rInferior = (Rfixo * Rcarga) / (Rfixo + Rcarga); - resistenciaNtc = rInferior * (Vs - vRetorno) / vRetorno; - } - - if (!isfinite(resistenciaNtc) || resistenciaNtc <= 0.0f) - return NAN; - - const float temperaturaK = 1.0f / ((1.0f / T0) + logf(resistenciaNtc / Rntc25) / Beta); - const float temperaturaC = temperaturaK - 273.15f + offset; - - if (!isfinite(temperaturaC) || temperaturaC < -40.0f || temperaturaC > 150.0f) { - return NAN; - } - - return roundf(temperaturaC * 100.0f) / 100.0f; - } - - }; - - +}; #endif diff --git a/Firmware/Sensoriamento/Sensoriamento_v24/Sensoriamento_v24.ino b/Firmware/Sensoriamento/Sensoriamento_v24/Sensoriamento_v24.ino index cd18dea3a..fdd588ebe 100644 --- a/Firmware/Sensoriamento/Sensoriamento_v24/Sensoriamento_v24.ino +++ b/Firmware/Sensoriamento/Sensoriamento_v24/Sensoriamento_v24.ino @@ -33,7 +33,7 @@ bool Conectado = false; bool Conectar = false; int _TaxaAmostragem = 1000; -bool RequisitarDados = false; +bool RequisitarDados = true; int latenciaLoop = 0; int qtdEnviosStatus = 0; int maxEnviosStatus = 5; @@ -76,9 +76,9 @@ uint32_t publicacaoMaxMs = 0; // Aquisição física é responsabilidade exclusiva da TelemetriaTask. // O caminho CAN apenas serializa os snapshots já disponíveis em memória. -static constexpr uint32_t PERIODO_CORRENTE_MS = 200; // 5 Hz -static constexpr uint32_t PERIODO_NTC_MS = 500; // 2 Hz -static constexpr uint32_t PERIODO_RETRY_I2C_MS = 10000; // retry lento +static constexpr uint32_t PERIODO_CORRENTE_MS = 500; // 2 Hz +static constexpr uint32_t PERIODO_NTC_MS = 1000; // 1 Hz +static constexpr uint32_t PERIODO_RETRY_I2C_MS = 3000; // retry lento static constexpr uint32_t PERIODO_RETRY_SENSOR_MS = 10000; static constexpr uint32_t PERIODO_DIAG_TELEMETRIA_MS = 10000; @@ -139,10 +139,14 @@ Rele_v2* ObterRelePorId(uint8_t idNum) { #pragma endregion #pragma region TEMPERATURA NTC -SensorTemperaturaNtc sNTC_MotorET("TMVET", 1, 6, TiposBarramentos::CONTROLADOR, -1, 0.0f, false); -SensorTemperaturaNtc sNTC_MotorDT("TMVDT", 3, 7, TiposBarramentos::CONTROLADOR, -1, 0.0f, true); -SensorTemperaturaNtc sNTC_MotorEF("TMVEF", 2, 8, TiposBarramentos::CONTROLADOR, -1, 0.0f, false); -SensorTemperaturaNtc sNTC_MotorDF("TMVDF", 4, 9, TiposBarramentos::CONTROLADOR, -1, 0.0f, true); +//SensorTemperaturaNtc sNTC_MotorET("TMVET", 1, 6, TiposBarramentos::CONTROLADOR, -1, 0.0f, false); +SensorTemperaturaNtc sNTC_MotorET("TMVET", 1, 0, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true); +//SensorTemperaturaNtc sNTC_MotorDT("TMVDT", 3, 7, TiposBarramentos::CONTROLADOR, -1, 0.0f, true); +SensorTemperaturaNtc sNTC_MotorDT("TMVDT", 3, 1, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true); +//SensorTemperaturaNtc sNTC_MotorEF("TMVEF", 2, 8, TiposBarramentos::CONTROLADOR, -1, 0.0f, false); +SensorTemperaturaNtc sNTC_MotorEF("TMVEF", 2, 2, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true); +//SensorTemperaturaNtc sNTC_MotorDF("TMVDF", 4, 9, TiposBarramentos::CONTROLADOR, -1, 0.0f, true); +SensorTemperaturaNtc sNTC_MotorDF("TMVDF", 4, 3, TiposBarramentos::EXPANSOR_ADS, -1, 0.0f, true); SensorTemperaturaNtc* ObterTempNtcPorId(uint8_t idNum) { switch (idNum) { @@ -195,6 +199,156 @@ SensorMagnetico_v2* ObterSensorMagneticoPorId(uint8_t idNum) { } #pragma endregion + + +#pragma region DIAGNOSTICO + +uint8_t SaturarByte(uint32_t valor) { + return valor > 255 ? 255 : (uint8_t)valor; +} + +uint16_t SaturarU16(uint32_t valor) { + return valor > 65535U ? 65535U : static_cast(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 MontarFrameDiagnosticoI2C(CanMessagePosicaoDados posicao) { + std::vector data; + + data.push_back(static_cast(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(maxAdsMs >> 8)); + + data.push_back(static_cast(maxAdsMs & 0xFF)); + + // bytes 4..5 + data.push_back(static_cast(timeoutsAds >> 8)); + + data.push_back(static_cast(timeoutsAds & 0xFF)); + + // bytes 6..7 + data.push_back(static_cast(readyDropsAds >> 8)); + + data.push_back(static_cast(readyDropsAds & 0xFF)); + + } + else if (posicao == CanMessagePosicaoDados::Dados4) { + const uint16_t maxStart = SaturarU16(diag.maxInicioAdsMs); + const uint16_t maxRead = SaturarU16(diag.maxResultadoAdsMs); + const uint16_t falhasIo = SaturarU16(diag.falhasIoAds); + + data.push_back((uint8_t)(maxStart >> 8)); + data.push_back((uint8_t)(maxStart & 0xFF)); + + data.push_back((uint8_t)(maxRead >> 8)); + data.push_back((uint8_t)(maxRead & 0xFF)); + + data.push_back((uint8_t)(falhasIo >> 8)); + data.push_back((uint8_t)(falhasIo & 0xFF)); + } + + return data; +} + +#pragma endregion + + + void MostrarLog(String mensagem) { bool _debugMode = false; if (_debugMode) { @@ -203,9 +357,9 @@ void MostrarLog(String mensagem) { } void inicializarDependenciasI2C() { - I2CService::DefinirPinos(1, 2, -1, -1, 39, 40); + I2CService::DefinirPinos(1, 2, 10, 11, 39, 40); I2CService::IniciarI2C(); - //I2CService::IniciarADS(); + I2CService::IniciarADS(); I2CService::IniciarMUX(); //I2CService::IniciarMCP(); } @@ -260,6 +414,30 @@ void TentarReinicializarSensoresCorrente() { } } +void TentarReinicializarSensoresNtc() { + if (!I2CService::I2CIniciado) + return; + + if (!I2CService::AdsIniciado) { + if (!I2CService::IniciarADS()) + return; + } + + SensorTemperaturaNtc* sensores[] = { + &sNTC_MotorET, + &sNTC_MotorDT, + &sNTC_MotorEF, + &sNTC_MotorDF + }; + + for (auto* sensor : sensores) { + if (sensor != nullptr && !sensor->EstaIniciado()) { + sensor->Inicializar(); + vTaskDelay(pdMS_TO_TICKS(10)); + } + } +} + void TelemetriaTask(void* pv) { TickType_t ultimaCorrente = 0; TickType_t ultimoNtc = 0; @@ -271,18 +449,18 @@ void TelemetriaTask(void* pv) { const TickType_t agora = xTaskGetTickCount(); if ((agora - ultimaCorrente) >= pdMS_TO_TICKS(PERIODO_CORRENTE_MS)) { - ultimaCorrente = agora; const uint32_t inicioAquisicao = millis(); AtualizarSensoresCorrente(); + ultimaCorrente = xTaskGetTickCount(); const uint32_t duracao = millis() - inicioAquisicao; if (duracao > maxAquisicaoCorrenteMs) maxAquisicaoCorrenteMs = duracao; ciclosTelemetria++; } if ((agora - ultimoNtc) >= pdMS_TO_TICKS(PERIODO_NTC_MS)) { - ultimoNtc = agora; const uint32_t inicioAquisicao = millis(); AtualizarSensoresNtc(); + ultimoNtc = xTaskGetTickCount(); const uint32_t duracao = millis() - inicioAquisicao; if (duracao > maxAquisicaoNtcMs) maxAquisicaoNtcMs = duracao; } @@ -295,6 +473,7 @@ void TelemetriaTask(void* pv) { if ((agora - ultimoRetrySensor) >= pdMS_TO_TICKS(PERIODO_RETRY_SENSOR_MS)) { ultimoRetrySensor = agora; TentarReinicializarSensoresCorrente(); + TentarReinicializarSensoresNtc(); } if ((agora - ultimoDiag) >= pdMS_TO_TICKS(PERIODO_DIAG_TELEMETRIA_MS)) { @@ -342,21 +521,33 @@ void setup() { sLED_Manual.Inicializar(); sLED_Automatico.Inicializar(); sLED_Operacao.Inicializar(); - sNTC_MotorET.Inicializar(); - sNTC_MotorDT.Inicializar(); - sNTC_MotorEF.Inicializar(); - sNTC_MotorDF.Inicializar(); - sCOR_3V0.Inicializar(); - sCOR_5V0.Inicializar(); - sCOR_7V2.Inicializar(); - sCOR_12V.Inicializar(); - sCOR_19V.Inicializar(); - sCOR_24V.Inicializar(); - sCOR_36V.Inicializar(); - //sMAG_MotorET.Inicializar(); - //sMAG_MotorDT.Inicializar(); - //sMAG_MotorEF.Inicializar(); - //sMAG_MotorDF.Inicializar(); + + if (I2CService::I2CIniciado) { + + if (I2CService::AdsIniciado) { + sNTC_MotorET.Inicializar(); + sNTC_MotorDT.Inicializar(); + sNTC_MotorEF.Inicializar(); + sNTC_MotorDF.Inicializar(); + } + + if (I2CService::MuxIniciado) { + sCOR_3V0.Inicializar(); + sCOR_5V0.Inicializar(); + sCOR_7V2.Inicializar(); + sCOR_12V.Inicializar(); + sCOR_19V.Inicializar(); + sCOR_24V.Inicializar(); + sCOR_36V.Inicializar(); + } + + //sMAG_MotorET.Inicializar(); + //sMAG_MotorDT.Inicializar(); + //sMAG_MotorEF.Inicializar(); + //sMAG_MotorDF.Inicializar(); + } + + ledManager.Adicionar(&sLED_Manual); ledManager.Adicionar(&sLED_Automatico); @@ -439,9 +630,33 @@ void enviarDadosSensores(uint8_t id_num, CanMessagePosicaoDados posicao) { bool todosIDs = id_num == canService.ID_Num_sTOD; if (id_num == canService.ID_Num_sMOD) { - EnviarDadosCAN(canService.MontarFrameReqStatusMod(D_Code, cfg.conectado, VERSION)); - if (PublicacaoMutex != nullptr) xSemaphoreGive(PublicacaoMutex); - return; + switch (posicao) { + case CanMessagePosicaoDados::Status: + EnviarDadosCAN(canService.MontarFrameReqStatusMod(D_Code, cfg.conectado, VERSION)); + break; + + case CanMessagePosicaoDados::Dados1: + case CanMessagePosicaoDados::Dados2: + case CanMessagePosicaoDados::Dados3: + case CanMessagePosicaoDados::Dados4: + EnviarDadosCAN(MontarFrameDiagnosticoI2C(posicao)); + break; + + default: + break; + } + + if (PublicacaoMutex != nullptr) + xSemaphoreGive(PublicacaoMutex); + + return; + } + + if (todosIDs && todosDados) { + EnviarDadosCAN(MontarFrameDiagnosticoI2C(CanMessagePosicaoDados::Dados1)); + EnviarDadosCAN(MontarFrameDiagnosticoI2C(CanMessagePosicaoDados::Dados2)); + EnviarDadosCAN(MontarFrameDiagnosticoI2C(CanMessagePosicaoDados::Dados3)); + EnviarDadosCAN(MontarFrameDiagnosticoI2C(CanMessagePosicaoDados::Dados4)); } if (todosIDs)