From 92a845aec013ae9b69dcc2226ce183816f67ee60 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Mon, 22 Jun 2026 16:09:41 -0300 Subject: [PATCH] ajustado calculo de vazao por fluxo, protocolo daly bms, protocolo oid, can service robusto --- .../AgroBase/Models/Modules/AtuadorModel.cs | 67 +- AgroBase/AgroBase/Models/OIDModel.cs | 3 +- AgroBase/AgroBase/Services/CANService.cs | 55 + .../AgroBase/Services/CanServiceEthernet.cs | 344 ++++- AgroBase/AgroBase/Services/DalyBMSService.cs | 929 +++++++++--- .../Services/HerbicideCounterService.cs | 44 +- AgroBase/AgroBase/Services/OIDCanService.cs | 1282 ++++++++++++++--- 7 files changed, 2149 insertions(+), 575 deletions(-) diff --git a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs index ad347fe2c..ff786dd45 100644 --- a/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs +++ b/AgroBase/AgroBase/Models/Modules/AtuadorModel.cs @@ -155,15 +155,13 @@ namespace AgroBase.Models.Modules { get { - double direta = VazaoFluxoMLpSInstantanea; - - if (direta > 0) - return direta; - - return VazaoFluxoMLpSInstantaneaCalc; + return ObterVazaoAtualConfiavelMLs(DateTime.Now); } } + [JsonIgnore] + public double VazaoMaximaSensor_mLs => VazaoMaximaSensor_LMin * 1000.0 / 60.0; + public double VazaoMediaMLs { get; set; } public double VolumeVazadoML { get; set; } public double TempoAtuado { get; set; } @@ -226,25 +224,38 @@ namespace AgroBase.Models.Modules { var sensor = SensorFluxoLinha; - if (sensor == null || !sensor.Inicializado || sensor.ValoresLeituras == null) + if (sensor == null || + !sensor.Inicializado || + sensor.ValoresLeituras == null) { return 0; } - var leituraFluxo = sensor.ValoresLeituras.FirstOrDefault(x => x.funcao == FuncoesPinout.Fluxo); + double maximaMLs = + VazaoMaximaSensor_LMin * + 1000.0 / + 60.0; + + var leituraFluxo = + sensor.ValoresLeituras.FirstOrDefault( + x => x.funcao == FuncoesPinout.Fluxo + ); /* - * A leitura direta é prioritária, inclusive quando vale zero. - * Zero recente é uma medição válida, não motivo para usar - * pulsos antigos como fallback. + * IMPORTANTE: + * A leitura direta FuncoesPinout.Fluxo já está em mL/s no modelo. + * Não converter novamente de L/min para mL/s. + * + * Zero recente é válido. Se a linha está sem fluxo, o sensor deve + * poder dizer zero sem cair no fallback por pulso antigo. */ if (LeituraSensorRecente(leituraFluxo, agora)) { - double vazaoLMin; + double vazaoMLs; try { - vazaoLMin = + vazaoMLs = Convert.ToDouble( leituraFluxo.atual.valor ); @@ -254,25 +265,23 @@ namespace AgroBase.Models.Modules return 0; } - if (double.IsNaN(vazaoLMin) || - double.IsInfinity(vazaoLMin) || - vazaoLMin < 0) + if (double.IsNaN(vazaoMLs) || + double.IsInfinity(vazaoMLs) || + vazaoMLs < 0) { return 0; } - vazaoLMin = FuncoesMatematicas.Clamp( - vazaoLMin, + return FuncoesMatematicas.Clamp( + vazaoMLs, 0.0, - VazaoMaximaSensor_LMin + maximaMLs ); - - return vazaoLMin * 1000.0 / 60.0; } /* - * Somente usa o cálculo por pulsos se os dois valores - * necessários também forem recentes. + * Fallback por pulso: + * aqui sim a fórmula calcula q em L/min e depois converte para mL/s. */ var leituraPulsos = sensor.ValoresLeituras.FirstOrDefault( @@ -300,11 +309,6 @@ namespace AgroBase.Models.Modules return 0; } - double maximaMLs = - VazaoMaximaSensor_LMin * - 1000.0 / - 60.0; - return FuncoesMatematicas.Clamp( calculadaMLs, 0.0, @@ -639,13 +643,10 @@ namespace AgroBase.Models.Modules volumeIntervaloML / bicosDoIntervalo.Count; - foreach (var bico in - bicosDoIntervalo) + foreach (var bico in bicosDoIntervalo) { bico.TempoComFluxo += dt; - - bico.VolumeVazadoML += - volumePorBicoML; + bico.VolumeVazadoML += volumePorBicoML; } } diff --git a/AgroBase/AgroBase/Models/OIDModel.cs b/AgroBase/AgroBase/Models/OIDModel.cs index 135f5a2e1..185e56bf0 100644 --- a/AgroBase/AgroBase/Models/OIDModel.cs +++ b/AgroBase/AgroBase/Models/OIDModel.cs @@ -258,7 +258,8 @@ namespace AgroBase.Models { public byte EnderecoTx { get; set; } public byte EnderecoRx { get; set; } - public bool Iniciado + public bool Iniciado { get; set; } + public bool Iniciado_old { get { diff --git a/AgroBase/AgroBase/Services/CANService.cs b/AgroBase/AgroBase/Services/CANService.cs index 329f5beef..4ce75c3c6 100644 --- a/AgroBase/AgroBase/Services/CANService.cs +++ b/AgroBase/AgroBase/Services/CANService.cs @@ -129,6 +129,61 @@ namespace AgroBase.Services return $"{Id}_{data[0]}_{parametro}"; } + public static string GerarChaveCorrelacao(T_Code dispositivo, uint idTx, uint idRx, byte funcCodeTx, byte funcCodeRx, byte[] dataTx) + { + dataTx = dataTx ?? new byte[0]; + + switch (dispositivo) + { + case T_Code.Bat: + /* + * Daly usa DataID no CAN ID estendido. + * Então o idRx já diferencia 0x90, 0x91, 0x95... + */ + return + $"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}"; + + case T_Code.Oid: + /* + * OID consulta: + * TX/RX DATA[0] = 0x0F + * DATA[1] = parâmetro. + */ + return + $"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}|fc:0x{funcCodeRx:X2}|param:{ByteKey(dataTx, 1)}"; + + case T_Code.Mks: + return + $"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}|fc:0x{funcCodeRx:X2}"; + + case T_Code.Atu: + case T_Code.Sen: + /* + * Protocolo dos módulos usa posição/funcCode e normalmente + * ID_Num em DATA[1]. + */ + return + $"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}|fc:0x{funcCodeRx:X2}|id_num:{ByteKey(dataTx, 1)}"; + + default: + return + $"{dispositivo}|tx:{IdKey(idTx)}|rx:{IdKey(idRx)}|fc:0x{funcCodeRx:X2}|p1:{ByteKey(dataTx, 1)}"; + } + } + + private static string ByteKey(byte[] data, int index) + { + if (data == null || data.Length <= index) + return "--"; + + return "0x" + data[index].ToString("X2"); + } + + public static string IdKey(uint id) + { + return $"0x{id:X}"; + } + } public class CanMessage diff --git a/AgroBase/AgroBase/Services/CanServiceEthernet.cs b/AgroBase/AgroBase/Services/CanServiceEthernet.cs index 46f6d8d44..49a7d66ed 100644 --- a/AgroBase/AgroBase/Services/CanServiceEthernet.cs +++ b/AgroBase/AgroBase/Services/CanServiceEthernet.cs @@ -107,7 +107,7 @@ namespace AgroBase.Services { return MensagensPendentes .Where(x => x.ComResposta && x.Enviado && !x.Respondido) - .GroupBy(x => IdKey(x.IdRx)) + .GroupBy(x => CanManager.IdKey(x.IdRx)) .ToDictionary(g => g.Key, g => g.Count()); } } @@ -139,6 +139,33 @@ namespace AgroBase.Services private DateTime _inicioJanelaLatencia = DateTime.UtcNow; private double _latenciaMaxJanelaMs = 0; + private readonly ConcurrentDictionary _cooldownPorCanal = + new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _ultimaFilaPorChave = + new ConcurrentDictionary(); + + private static readonly TimeSpan COOLDOWN_DEFAULT = + TimeSpan.FromMilliseconds(0); + + private static readonly TimeSpan COOLDOWN_MKS = + TimeSpan.FromMilliseconds(20); + + private static readonly TimeSpan COOLDOWN_OID = + TimeSpan.FromMilliseconds(8); + + private static readonly TimeSpan COOLDOWN_BAT = + TimeSpan.FromMilliseconds(80); + + private static readonly TimeSpan COOLDOWN_ATU_SEN = + TimeSpan.FromMilliseconds(4); + + private static readonly TimeSpan DEDUPE_JANELA_GET = + TimeSpan.FromMilliseconds(250); + + private static readonly TimeSpan DEDUPE_JANELA_SET = + TimeSpan.FromMilliseconds(60); + private static long NowUnixMs() { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -340,6 +367,16 @@ namespace AgroBase.Services if (dados.Count > 8) throw new InvalidOperationException($"Payload CAN maior que 8 bytes. ID=0x{idTx:X}, Len={dados.Count}"); + string chaveCorrelacao = + CanManager.GerarChaveCorrelacao( + Dispositivo, + idTx, + idRx, + funcCodeTx, + funcCodeRx, + dados.ToArray() + ); + var novaMensagem = new CanMessage { Momento = DateTime.Now, @@ -350,25 +387,70 @@ namespace AgroBase.Services funcCodeRx = funcCodeRx, DataTx = dados.ToArray(), ComResposta = get, - ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray()), + ChaveInterna = chaveCorrelacao, ContabilizarTimeoutHealth = contabilizarTimeoutHealth, MensagemDescoberta = mensagemDescoberta }; lock (_LockMensagens) { - bool mensagemNaFila = MensagensPendentes.Any(x => - (!x.Enviado || (x.ComResposta && x.Enviado && !x.Respondido)) && - x.IdTx == idTx && - x.IdRx == idRx && - x.DataTx != null && - x.DataTx.SequenceEqual(dados.ToArray())); + DateTime agora = + DateTime.Now; - if (!mensagemNaFila) + /* + * Para request com resposta, não deixa duas pendências iguais + * ficarem esperando a mesma resposta. Isso é vital para OID, porque + * todas as consultas respondem com funcCode 0x0F e diferem pelo param. + */ + bool mesmaPendenciaAberta = + MensagensPendentes.Any(x => + x.ComResposta && + !x.Respondido && + x.ChaveInterna == chaveCorrelacao); + + if (get && mesmaPendenciaAberta) + return; + + /* + * Para comando sem resposta, evita empilhar comandos idênticos + * ainda não enviados. Depois de enviado, ele será limpo normalmente. + */ + bool comandoIdenticoNaoEnviado = + MensagensPendentes.Any(x => + !x.ComResposta && + !x.Enviado && + x.IdTx == idTx && + x.DataTx != null && + x.DataTx.SequenceEqual(dados.ToArray())); + + if (!get && comandoIdenticoNaoEnviado) + return; + + /* + * Dedupe temporal extra, útil para comandos de controle que chegam + * repetidos por loop rápido. + */ + TimeSpan janela = + get + ? DEDUPE_JANELA_GET + : DEDUPE_JANELA_SET; + + if (_ultimaFilaPorChave.TryGetValue( + chaveCorrelacao, + out DateTime ultima) && + agora - ultima < janela) { - MensagensPendentes.Add(novaMensagem); - MostrarLog($"Mensagem adicionada. ID=0x{idTx:X}, Data={BitConverter.ToString(novaMensagem.DataTx)}", idTx); + return; } + + _ultimaFilaPorChave[chaveCorrelacao] = agora; + + MensagensPendentes.Add(novaMensagem); + + MostrarLog( + $"Mensagem adicionada. ID=0x{idTx:X}, Data={BitConverter.ToString(novaMensagem.DataTx)}", + idTx + ); } } @@ -402,23 +484,62 @@ namespace AgroBase.Services } private readonly ConcurrentDictionary _cooldownPorId = new ConcurrentDictionary(); - private static readonly TimeSpan COOLDOWN_MKS = TimeSpan.FromMilliseconds(20); private bool PodeEnviarAgora(CanMessage m) { - if (m.Dispositivo != T_Code.Mks) + if (m == null) + return false; + + TimeSpan cooldown = + ObterCooldownDispositivo(m.Dispositivo); + + if (cooldown <= TimeSpan.Zero) return true; - var now = DateTime.UtcNow; - var due = _cooldownPorId.GetOrAdd(m.IdTx, DateTime.MinValue); + string chave = + $"{m.Dispositivo}|tx:{CanManager.IdKey(m.IdTx)}"; + + var now = + DateTime.UtcNow; + + var due = + _cooldownPorCanal.GetOrAdd( + chave, + DateTime.MinValue + ); if (now < due) return false; - _cooldownPorId[m.IdTx] = now + COOLDOWN_MKS; + _cooldownPorCanal[chave] = + now + cooldown; + return true; } + private static TimeSpan ObterCooldownDispositivo(T_Code dispositivo) + { + switch (dispositivo) + { + case T_Code.Mks: + return COOLDOWN_MKS; + + case T_Code.Oid: + return COOLDOWN_OID; + + case T_Code.Bat: + return COOLDOWN_BAT; + + case T_Code.Atu: + case T_Code.Sen: + return COOLDOWN_ATU_SEN; + + default: + return COOLDOWN_DEFAULT; + } + } + + private async Task EnviarCAN() { if (!IsConnected) return; @@ -498,7 +619,7 @@ namespace AgroBase.Services } _tcpStream.Write(packet, 0, packet.Length); - _tcpStream.Flush(); + //_tcpStream.Flush(); } else { @@ -686,29 +807,17 @@ namespace AgroBase.Services lock (_LockMensagens) { - mensagem = MensagensPendentes.FirstOrDefault(x => - { - if (!x.Enviado) return false; - if (x.Respondido) return false; - if (x.Dispositivo != dispositivo) return false; - if (x.IdRx != id) return false; - if (x.DataTx == null || x.DataTx.Length == 0) return false; - - if (db) - return true; - - // MKS: a resposta deve casar pelo function code. - // TX: [code, ..., crc] - // RX: [code, ..., crc] - if (x.Dispositivo == T_Code.Mks) - return data.Length > 0 && x.funcCodeRx == data[0]; - - // Demais protocolos antigos - if (data.Length < 2 || x.DataTx.Length < 2) - return true; - - return x.DataTx[1] == data[1]; - }); + mensagem = + MensagensPendentes + .Where(x => + MensagemCorrespondeResposta( + x, + dispositivo, + id, + data + )) + .OrderBy(x => x.EnviadoEm) + .FirstOrDefault(); } if (mensagem == null) @@ -745,6 +854,112 @@ namespace AgroBase.Services } } + private bool MensagemCorrespondeResposta(CanMessage pendente, T_Code dispositivoResposta, uint idResposta, byte[] dataResposta) + { + if (pendente == null) + return false; + + if (!pendente.Enviado) + return false; + + if (pendente.Respondido) + return false; + + if (!pendente.ComResposta) + return false; + + if (pendente.Dispositivo != dispositivoResposta) + return false; + + if (pendente.IdRx != idResposta) + return false; + + byte[] dataTx = + pendente.DataTx ?? new byte[0]; + + byte[] dataRx = + dataResposta ?? new byte[0]; + + switch (pendente.Dispositivo) + { + case T_Code.Bat: + /* + * Daly: DataID vem no CAN ID, então idRx já fechou. + */ + return true; + + case T_Code.Mks: + return + dataRx.Length > 0 && + pendente.funcCodeRx == dataRx[0]; + + case T_Code.Oid: + /* + * OID: + * consulta retorna DATA[0]=0x0F e DATA[1]=param. + */ + return + dataRx.Length >= 2 && + dataTx.Length >= 2 && + dataRx[0] == pendente.funcCodeRx && + dataRx[1] == dataTx[1]; + + case T_Code.Atu: + case T_Code.Sen: + /* + * ATU/SEN: + * casa pelo func/posição e, quando existir, pelo ID_Num. + */ + if (dataRx.Length <= 0) + return false; + + if (dataRx[0] != pendente.funcCodeRx) + return false; + + if (dataRx.Length >= 2 && dataTx.Length >= 2) + return dataRx[1] == dataTx[1]; + + return true; + + default: + if (dataRx.Length <= 0) + return false; + + if (dataRx[0] != pendente.funcCodeRx) + return false; + + if (dataRx.Length >= 2 && dataTx.Length >= 2) + return dataRx[1] == dataTx[1]; + + return true; + } + } + + private static TimeSpan TimeoutResposta(CanMessage msg) + { + if (msg == null) + return TimeSpan.FromSeconds(2); + + switch (msg.Dispositivo) + { + case T_Code.Oid: + return TimeSpan.FromMilliseconds(900); + + case T_Code.Bat: + return TimeSpan.FromMilliseconds(1500); + + case T_Code.Mks: + return TimeSpan.FromMilliseconds(900); + + case T_Code.Atu: + case T_Code.Sen: + return TimeSpan.FromMilliseconds(1200); + + default: + return TimeSpan.FromSeconds(2); + } + } + private void LimparMensagensAntigas() { var agora = DateTime.Now; @@ -756,7 +971,8 @@ namespace AgroBase.Services x.ComResposta && x.Enviado && !x.Respondido && - x.EnviadoEm < agora.AddSeconds(-2)) + x.EnviadoEm != DateTime.MinValue && + agora - x.EnviadoEm > TimeoutResposta(x)) .ToList(); foreach (var item in timeouts) @@ -765,12 +981,14 @@ namespace AgroBase.Services MensagensPendentes.Remove(item); } - var remover = MensagensPendentes - .Where(x => - (!x.ComResposta && x.Enviado) || - (x.ComResposta && x.Respondido) || - (x.Momento < agora.AddSeconds(-30))) - .ToList(); + var remover = + MensagensPendentes + .Where(x => + (!x.ComResposta && x.Enviado) || + (x.ComResposta && x.Respondido) || + (x.Momento < agora.AddSeconds(-30)) || + (!IsConnected && x.Enviado)) + .ToList(); foreach (var item in remover) MensagensPendentes.Remove(item); @@ -836,6 +1054,8 @@ namespace AgroBase.Services { try { + LimparPendenciasEnviadasPorReconexao(); + FecharConexaoSemLimparHandlers(); if (TransportMode == CanEthernetTransportMode.Tcp) @@ -863,6 +1083,22 @@ namespace AgroBase.Services } } + private void LimparPendenciasEnviadasPorReconexao() + { + lock (_LockMensagens) + { + var remover = + MensagensPendentes + .Where(x => + x.Enviado || + x.Momento < DateTime.Now.AddSeconds(-2)) + .ToList(); + + foreach (var item in remover) + MensagensPendentes.Remove(item); + } + } + private void FecharConexaoSemLimparHandlers() { lock (_socketLock) @@ -1032,17 +1268,12 @@ namespace AgroBase.Services } } - private static string IdKey(uint id) - { - return $"0x{id:X}"; - } - private static string TimeoutChave(CanMessage msg) { if (msg == null) return "desconhecido"; - string chave = $"{msg.Dispositivo}|tx:{IdKey(msg.IdTx)}|rx:{IdKey(msg.IdRx)}|fc_rx:0x{msg.funcCodeRx:X2}"; + string chave = $"{msg.Dispositivo}|tx:{CanManager.IdKey(msg.IdTx)}|rx:{CanManager.IdKey(msg.IdRx)}|fc_rx:0x{msg.funcCodeRx:X2}"; if (new List() { T_Code.Atu, T_Code.Sen }.Contains(msg.Dispositivo) && msg.DataTx != null && msg.DataTx.Length > 1) { @@ -1054,6 +1285,11 @@ namespace AgroBase.Services chave += $"|fc_tx:0x{msg.DataTx[0]:X2}"; } + if (!string.IsNullOrWhiteSpace(msg.ChaveInterna)) + { + chave += $"|key:{msg.ChaveInterna}"; + } + return chave; } @@ -1075,7 +1311,7 @@ namespace AgroBase.Services TimeoutsRespostaTotal++; - string idRx = IdKey(msg.IdRx); + string idRx = CanManager.IdKey(msg.IdRx); string dispositivo = msg.Dispositivo.ToString(); string chave = TimeoutChave(msg); diff --git a/AgroBase/AgroBase/Services/DalyBMSService.cs b/AgroBase/AgroBase/Services/DalyBMSService.cs index 774fe7fcb..d617812d2 100644 --- a/AgroBase/AgroBase/Services/DalyBMSService.cs +++ b/AgroBase/AgroBase/Services/DalyBMSService.cs @@ -1,7 +1,8 @@ -using AgroBase.Models; +using AgroBase.Models; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using static AgroBase.Models.Enums; using static AgroBase.Services.DalyBMSService.BatHandler; @@ -10,127 +11,448 @@ namespace AgroBase.Services { public class DalyBMSService { - public static DalyBmsModel DadosLeitura { get; set; } = new DalyBmsModel(); - private static T_Code Dispositivo { get; set; } = T_Code.Bat; - public static BatHandler _BatHandler { get; set; } = new BatHandler(); + public static DalyBmsModel DadosLeitura { get; set; } = + new DalyBmsModel(); + + private static T_Code Dispositivo { get; set; } = + T_Code.Bat; + + public static BatHandler _BatHandler { get; set; } = + new BatHandler(); + + private static readonly object _sync = + new object(); + + private static readonly object _handlerLock = + new object(); + + private static bool _handlersRegistrados; + + private static readonly TimeSpan TempoRespostaRecente = + TimeSpan.FromSeconds(12); + + private static readonly TimeSpan TempoRemoverMapeamento = + TimeSpan.FromSeconds(60); + + private static readonly TimeSpan IntervaloMinimoEntreRequests = + TimeSpan.FromMilliseconds(250); + + private static readonly TimeSpan IntervaloDescoberta = + TimeSpan.FromSeconds(3); + + private static readonly Dictionary + IntervalosPorFuncao = + new Dictionary + { + /* + * Polling rápido, dados realmente úteis para runtime. + */ + { BatFuncCode.SOC, TimeSpan.FromSeconds(1) }, + + /* + * Estado geral/MOS não precisa bater a cada ciclo. + */ + { BatFuncCode.CargaDescargaMOS, TimeSpan.FromSeconds(3) }, + { BatFuncCode.StatusInformacao, TimeSpan.FromSeconds(3) }, + + /* + * Max/min são leves. + */ + { BatFuncCode.TensaoMaxMin, TimeSpan.FromSeconds(5) }, + { BatFuncCode.TemperaturaMaxMin, TimeSpan.FromSeconds(5) }, + + /* + * Alarmes e balanceamento. + */ + { BatFuncCode.StatusFalhaBateria, TimeSpan.FromSeconds(8) }, + { BatFuncCode.StatusBalanceamentoCelula, TimeSpan.FromSeconds(15) }, + + /* + * Dados multi-frame. Pedir devagar para não saturar o Daly. + * 0x95 pode responder até 16 frames. + * 0x96 pode responder até 3 frames. + */ + { BatFuncCode.TensaoCelula, TimeSpan.FromSeconds(20) }, + { BatFuncCode.TemperaturaCelula, TimeSpan.FromSeconds(20) }, + }; + + private static readonly Dictionary + _ultimoPedido = + Enum + .GetValues(typeof(BatFuncCode)) + .Cast() + .ToDictionary(x => x, x => DateTime.MinValue); + + private static DateTime _ultimaTentativaDescoberta = + DateTime.MinValue; + + private static DateTime _ultimoEnvioCan = + DateTime.MinValue; + + /* + * Mantém prioridade determinística. + * O método EscolherProximoDadoParaRequisitar() varre essa lista e + * envia no máximo um request por chamada de AtualizarDados(). + */ + private static readonly BatFuncCode[] OrdemPolling = + { + BatFuncCode.SOC, + BatFuncCode.CargaDescargaMOS, + BatFuncCode.StatusInformacao, + BatFuncCode.TensaoMaxMin, + BatFuncCode.TemperaturaMaxMin, + BatFuncCode.StatusFalhaBateria, + BatFuncCode.StatusBalanceamentoCelula, + BatFuncCode.TensaoCelula, + BatFuncCode.TemperaturaCelula + }; + + private static int _cursorPolling; + public static bool Iniciado { get { - return CanManager.CanService.IsConnected && SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo) && DadosLeitura.Iniciado; + return + CanManager.CanService.IsConnected && + DispositivoMapeado && + DadosLeitura.Iniciado && + RespostaRecente; } } - private static double TempoAtualizacao { get; set; } = 2; - private static DateTime UltimaAtualizacao { get; set; } = DateTime.MinValue; - - public static async Task VerificaDispositivoConectado() + private static bool DispositivoMapeado { - if (!CanManager.CanService.IsConnected) return false; - - foreach (BatFuncCode f in Enum.GetValues(typeof(BatFuncCode))) + get { - CanManager.CanService.RegistrarHandler(CanManager.ConverterIdCan(MakeResponseID(f)), _BatHandler); + return + SerialService + .DispositivosMapeados + .Any(x => x.Dispositivo == Dispositivo); } + } - EnviarComandoDescoberta(); - - DateTime Inicio = DateTime.Now; - bool Respondido() => DadosLeitura.UltimoComandoRecebido > Inicio; - await FuncoesGlobais.AguardarCondicaoAsync(Respondido, 500, 50); - - if (Respondido()) + private static bool RespostaRecente + { + get { - DefinirDispositivo(); - } + DateTime? ultimo = DadosLeitura.UltimoComandoRecebido; - return DadosLeitura.Iniciado; - } - - private static void DefinirDispositivo() - { - if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == Dispositivo)) - { - if (DadosLeitura != null) - { - SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel() - { - Dispositivo = Dispositivo, - Endereco = CanManager.CanService._portName, - Versao = ("1").ToString() - }); - } + return + ultimo != DateTime.MinValue && + DateTime.Now - ultimo <= TempoRespostaRecente; } } - - - public static void AtualizarDados() - { - if (!Iniciado) - { - var d = SerialService.DispositivosMapeados.FirstOrDefault(x => x.Dispositivo == Dispositivo); - if (d != null) - SerialService.DispositivosMapeados.Remove(d); - return; - } - - double dt = (DateTime.Now - UltimaAtualizacao).TotalSeconds; - if (dt < TempoAtualizacao) - return; - - RequisitarDado(BatFuncCode.SOC); - RequisitarDado(BatFuncCode.TensaoMaxMin); - RequisitarDado(BatFuncCode.TensaoCelula); - RequisitarDado(BatFuncCode.TemperaturaMaxMin); - RequisitarDado(BatFuncCode.TemperaturaCelula); - RequisitarDado(BatFuncCode.CargaDescargaMOS); - RequisitarDado(BatFuncCode.StatusInformacao); - RequisitarDado(BatFuncCode.StatusBalanceamentoCelula); - RequisitarDado(BatFuncCode.StatusFalhaBateria); - - UltimaAtualizacao = DateTime.Now; - } - - #region Helpers de protocolo (ID, DataID, etc.) - - private static void EnviarComandoDescoberta() - { - BatFuncCode dado = BatFuncCode.SOC; - CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, MakeRequestID(dado), MakeResponseID(dado), (byte)dado, (byte)dado, payload: new byte[8], get: true, contabilizarTimeoutHealth: false, mensagemDescoberta: true); - } - - private static void EnviarComando(T_Code Dispositivo, byte[] idTx, byte[] idRx, byte funcCode, byte[] payload = null) - { - CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, idTx, idRx, funcCode, funcCode, payload, get: true); - } - - // Endereços lógicos que você quiser adotar para PC e BMS. - // São os "AddrA" e "AddrB" da fórmula 0x18 [DataID] [AddrA] [AddrB]. public static byte PriorityAddr { get; set; } = 0x18; public static byte BmsAddr { get; set; } = 0x01; public static byte PcAddr { get; set; } = 0x40; - public static BatFuncCode GetDataId(uint canId) + public static async Task VerificaDispositivoConectado() { - return (BatFuncCode)((canId >> 16) & 0xFF); + if (!CanManager.CanService.IsConnected) + return false; + + RegistrarHandlersUmaVez(); + + DateTime inicio = DateTime.Now; + + EnviarComandoDescoberta(); + + bool Respondido() => + DadosLeitura.UltimoComandoRecebido > inicio; + + await FuncoesGlobais + .AguardarCondicaoAsync( + Respondido, + 1200, + 50 + ); + + if (Respondido()) + DefinirDispositivo(); + + return DadosLeitura.Iniciado; + } + + public static void AtualizarDados() + { + try + { + if (!CanManager.CanService.IsConnected) + return; + + RegistrarHandlersUmaVez(); + + DateTime agora = DateTime.Now; + + /* + * Se ainda não mapeou, tenta descoberta em ritmo baixo. + * Não martela o barramento. + */ + if (!DispositivoMapeado) + { + if (agora - _ultimaTentativaDescoberta >= + IntervaloDescoberta) + { + _ultimaTentativaDescoberta = agora; + EnviarComandoDescoberta(); + } + + return; + } + + /* + * Não remove o BAT na primeira falha. + * O Daly pode atrasar ou ignorar uma pergunta pesada. + * Remove só se sumiu de verdade por bastante tempo. + */ + if (DadosLeitura.Iniciado && + !RespostaRecente && + DadosLeitura.UltimoComandoRecebido != DateTime.MinValue && + agora - DadosLeitura.UltimoComandoRecebido > + TempoRemoverMapeamento) + { + RemoverMapeamento(); + DadosLeitura.Iniciado = false; + return; + } + + if (agora - _ultimoEnvioCan < + IntervaloMinimoEntreRequests) + { + return; + } + + BatFuncCode? proximo = + EscolherProximoDadoParaRequisitar(agora); + + if (!proximo.HasValue) + return; + + bool contabilizarTimeout = + DeveContabilizarTimeoutHealth(proximo.Value); + + RequisitarDado( + proximo.Value, + contabilizarTimeout + ); + + _ultimoPedido[proximo.Value] = agora; + _ultimoEnvioCan = agora; + } + catch (Exception ex) + { + Console.WriteLine( + "[DalyBMSService] Erro em AtualizarDados: " + + ex.Message + ); + } + } + + private static BatFuncCode? EscolherProximoDadoParaRequisitar( + DateTime agora) + { + for (int tentativa = 0; + tentativa < OrdemPolling.Length; + tentativa++) + { + int index = + (_cursorPolling + tentativa) % + OrdemPolling.Length; + + BatFuncCode codigo = + OrdemPolling[index]; + + TimeSpan intervalo = + IntervalosPorFuncao[codigo]; + + DateTime ultimo = + _ultimoPedido[codigo]; + + if (ultimo == DateTime.MinValue || + agora - ultimo >= intervalo) + { + _cursorPolling = + (index + 1) % + OrdemPolling.Length; + + return codigo; + } + } + + return null; + } + + private static bool DeveContabilizarTimeoutHealth( + BatFuncCode dado) + { + /* + * Apenas requests leves e críticos entram na saúde CAN. + * Dados multi-frame e secundários podem falhar sem dizer que + * a bateria caiu do barramento. + */ + return + dado == BatFuncCode.SOC || + dado == BatFuncCode.StatusInformacao || + dado == BatFuncCode.CargaDescargaMOS; + } + + private static void RegistrarHandlersUmaVez() + { + lock (_handlerLock) + { + if (_handlersRegistrados) + return; + + foreach (BatFuncCode f in Enum.GetValues( + typeof(BatFuncCode))) + { + CanManager + .CanService + .RegistrarHandler( + CanManager + .ConverterIdCan( + MakeResponseID(f) + ), + _BatHandler + ); + } + + _handlersRegistrados = true; + } + } + + private static void DefinirDispositivo() + { + if (DispositivoMapeado) + return; + + if (DadosLeitura == null) + return; + + SerialService + .DispositivosMapeados + .Add( + new DispositivoDetalhesModel + { + Dispositivo = Dispositivo, + Endereco = CanManager.CanService._portName, + Versao = "1" + } + ); + } + + private static void RemoverMapeamento() + { + var d = + SerialService + .DispositivosMapeados + .FirstOrDefault( + x => x.Dispositivo == Dispositivo + ); + + if (d != null) + SerialService.DispositivosMapeados.Remove(d); + } + + #region Helpers de protocolo + + private static void EnviarComandoDescoberta() + { + BatFuncCode dado = + BatFuncCode.SOC; + + CanManager + .CanService + .AdicionarMensagemNaFila( + Dispositivo, + MakeRequestID(dado), + MakeResponseID(dado), + (byte)dado, + (byte)dado, + payload: new byte[8], + get: true, + contabilizarTimeoutHealth: false, + mensagemDescoberta: true + ); + } + + private static void EnviarComando( + T_Code dispositivo, + byte[] idTx, + byte[] idRx, + byte funcCode, + byte[] payload = null, + bool contabilizarTimeoutHealth = true) + { + CanManager + .CanService + .AdicionarMensagemNaFila( + dispositivo, + idTx, + idRx, + funcCode, + funcCode, + payload ?? new byte[8], + get: true, + contabilizarTimeoutHealth: + contabilizarTimeoutHealth + ); } private static byte[] MakeRequestID(BatFuncCode dado) { - return new byte[4] { PriorityAddr, (byte)dado, BmsAddr, PcAddr }; - //return (uint)((0x18 << 24) | ((byte)dado << 16) | (BmsAddr << 8) | PcAddr); + /* + * Daly: + * PC -> BMS: + * Priority + DataID + BMS Address + PC Address + * Ex.: 0x18 0x90 0x01 0x40 + */ + return new byte[4] + { + PriorityAddr, + (byte)dado, + BmsAddr, + PcAddr + }; } private static byte[] MakeResponseID(BatFuncCode dado) { - return new byte[4] { PriorityAddr, (byte)dado, PcAddr, BmsAddr }; - //return (uint)((0x18 << 24) | ((byte)dado << 16) | (PcAddr << 8) | BmsAddr); + /* + * Daly: + * BMS -> PC: + * Priority + DataID + PC Address + BMS Address + * Ex.: 0x18 0x90 0x40 0x01 + */ + return new byte[4] + { + PriorityAddr, + (byte)dado, + PcAddr, + BmsAddr + }; } - private static void RequisitarDado(BatFuncCode dado) + private static void RequisitarDado( + BatFuncCode dado, + bool contabilizarTimeoutHealth) { - EnviarComando(Dispositivo, MakeRequestID(dado), MakeResponseID(dado), (byte)dado, payload: new byte[8]); + EnviarComando( + Dispositivo, + MakeRequestID(dado), + MakeResponseID(dado), + (byte)dado, + payload: new byte[8], + contabilizarTimeoutHealth: + contabilizarTimeoutHealth + ); + } + + public static BatFuncCode GetDataId(uint canId) + { + return (BatFuncCode)((canId >> 16) & 0xFF); } private static ushort DecodeUInt16BE(byte hi, byte lo) @@ -142,7 +464,8 @@ namespace AgroBase.Services public class BatHandler : ICanMessageHandler { - public T_Code Dispositivo { get; set; } = DalyBMSService.Dispositivo; + public T_Code Dispositivo { get; set; } = + DalyBMSService.Dispositivo; public enum BatFuncCode : byte { @@ -159,194 +482,284 @@ namespace AgroBase.Services public void ProcessarMensagem(CanMessage mensagem) { - if (mensagem == null) return; - if (mensagem.DataRx == null || mensagem.DataRx.Length < 8) return; - - // TODO: trocar "mensagem.CanId" pelo nome real da propriedade que guarda o ID CAN 29 bits - uint canId = mensagem.IdRx; - - BatFuncCode dataId = GetDataId(canId); - byte[] data = mensagem.DataRx; - - lock (DadosLeitura) + try { - DadosLeitura.Momento = DateTime.Now; - DadosLeitura.Iniciado = true; - DadosLeitura.UltimoComandoRecebido = DateTime.Now; + if (mensagem == null) + return; - // Guarda payload bruto por DataID para debug - DadosLeitura.LeiturasBrutas[(byte)dataId] = data.ToArray(); - - switch (dataId) + if (mensagem.DataRx == null || + mensagem.DataRx.Length < 8) { - case BatFuncCode.SOC: - Atualizar090_PackVoltageCurrentSoc(data); - break; - case BatFuncCode.TensaoMaxMin: - Atualizar091_CellMaxMin(data); - break; - case BatFuncCode.TemperaturaMaxMin: - Atualizar092_TempMaxMin(data); - break; - case BatFuncCode.CargaDescargaMOS: - Atualizar093_MosEstadoCapacidade(data); - break; - case BatFuncCode.StatusInformacao: - Atualizar094_StatusGeralIo(data); - break; - case BatFuncCode.TensaoCelula: - Atualizar095_TensoesCelulaFrame(data); - break; - case BatFuncCode.TemperaturaCelula: - Atualizar096_TemperaturasFrame(data); - break; - case BatFuncCode.StatusBalanceamentoCelula: - Atualizar097_Balanceamento(data); - break; - case BatFuncCode.StatusFalhaBateria: - Atualizar098_Alarmes(data); - break; - default: - // Outros DataID (se existirem) podem ser tratados aqui no futuro. - break; + return; } + + uint canId = + mensagem.IdRx; + + BatFuncCode dataId = + GetDataId(canId); + + byte[] data = + mensagem + .DataRx + .Take(8) + .ToArray(); + + lock (DadosLeitura) + { + DadosLeitura.Momento = DateTime.Now; + DadosLeitura.Iniciado = true; + DadosLeitura.UltimoComandoRecebido = + DateTime.Now; + + DadosLeitura.LeiturasBrutas[(byte)dataId] = + data.ToArray(); + + switch (dataId) + { + case BatFuncCode.SOC: + Atualizar090_PackVoltageCurrentSoc(data); + break; + + case BatFuncCode.TensaoMaxMin: + Atualizar091_CellMaxMin(data); + break; + + case BatFuncCode.TemperaturaMaxMin: + Atualizar092_TempMaxMin(data); + break; + + case BatFuncCode.CargaDescargaMOS: + Atualizar093_MosEstadoCapacidade(data); + break; + + case BatFuncCode.StatusInformacao: + Atualizar094_StatusGeralIo(data); + break; + + case BatFuncCode.TensaoCelula: + Atualizar095_TensoesCelulaFrame(data); + break; + + case BatFuncCode.TemperaturaCelula: + Atualizar096_TemperaturasFrame(data); + break; + + case BatFuncCode.StatusBalanceamentoCelula: + Atualizar097_Balanceamento(data); + break; + + case BatFuncCode.StatusFalhaBateria: + Atualizar098_Alarmes(data); + break; + } + } + + DefinirDispositivo(); + } + catch (Exception ex) + { + Console.WriteLine( + "[DalyBMSService] Erro ao processar BAT: " + + ex.Message + ); } } private void Atualizar090_PackVoltageCurrentSoc(byte[] d) { - // 0..1: total voltage (0.1 V) - // 2..3: gather voltage (0.1 V) - // 4..5: current (offset 30000, 0.1 A) - // 6..7: SOC (0.1 %) ushort totalVRaw = DecodeUInt16BE(d[0], d[1]); ushort measVRaw = DecodeUInt16BE(d[2], d[3]); ushort currRaw = DecodeUInt16BE(d[4], d[5]); ushort socRaw = DecodeUInt16BE(d[6], d[7]); - double totalV = totalVRaw / 10.0; - double measV = measVRaw / 10.0; - double current = (currRaw - 30000) / 10.0; - double soc = socRaw / 10.0; + double totalV = + totalVRaw / 10.0; + + double measV = + measVRaw / 10.0; + + double current = + (currRaw - 30000) / 10.0; + + double soc = + socRaw / 10.0; DadosLeitura.Pack.TensaoTotal_V = totalV; DadosLeitura.Pack.TensaoMedida_V = measV; + + /* + * Mantive o sinal usado no serviço original. + * Se o log mostrar corrente invertida, a correção é só aqui. + */ DadosLeitura.Pack.Corrente_A = -current; DadosLeitura.Pack.SOC_percent = soc; } private void Atualizar091_CellMaxMin(byte[] d) { - // 0..1: Vmax (mV) - // 2 : index max - // 3..4: Vmin (mV) - // 5 : index min - ushort vmaxRaw = DecodeUInt16BE(d[0], d[1]); - byte idxMax = d[2]; - ushort vminRaw = DecodeUInt16BE(d[3], d[4]); - byte idxMin = d[5]; + ushort vmaxRaw = + DecodeUInt16BE(d[0], d[1]); - double vmax = vmaxRaw / 1000.0; - double vmin = vminRaw / 1000.0; + byte idxMax = + d[2]; - // Marca flags nas células existentes - foreach (var c in DadosLeitura.Celulas) - { - c.EhMaxima = (c.Index == idxMax); - c.EhMinima = (c.Index == idxMin); - } + ushort vminRaw = + DecodeUInt16BE(d[3], d[4]); + + byte idxMin = + d[5]; + + double vmax = + vmaxRaw / 1000.0; + + double vmin = + vminRaw / 1000.0; - // Atualiza no modelo principal DadosLeitura.TensaoMax = vmax; DadosLeitura.TensaoMin = vmin; + + foreach (var c in DadosLeitura.Celulas) + { + c.EhMaxima = c.Index == idxMax; + c.EhMinima = c.Index == idxMin; + } } private void Atualizar092_TempMaxMin(byte[] d) { - // T = raw - 40 - int tmax = d[0] - 40; - byte idxMax = d[1]; - int tmin = d[2] - 40; - byte idxMin = d[3]; + int tmax = + d[0] - 40; + + byte idxMax = + d[1]; + + int tmin = + d[2] - 40; + + byte idxMin = + d[3]; DadosLeitura.TemperaturaMax = tmax; DadosLeitura.TemperaturaMin = tmin; foreach (var t in DadosLeitura.Temperaturas) { - t.EhMaxima = (t.Index == idxMax); - t.EhMinima = (t.Index == idxMin); + t.EhMaxima = t.Index == idxMax; + t.EhMinima = t.Index == idxMin; } } private void Atualizar093_MosEstadoCapacidade(byte[] d) { - // 0: state (0=idle,1=charge,2=discharge) - // 1: charge MOS - // 2: discharge MOS - // 3: life (raw) - // 4..7: remaining capacity (mAh) - byte state = d[0]; - byte chargeMos = d[1]; - byte dischgMos = d[2]; - byte lifeRaw = d[3]; - uint remainmAh = (uint)((d[4] << 24) | (d[5] << 16) | (d[6] << 8) | d[7]); + byte state = + d[0]; + + byte chargeMos = + d[1]; + + byte dischgMos = + d[2]; + + byte lifeRaw = + d[3]; + + uint remainmAh = + ((uint)d[4] << 24) | + ((uint)d[5] << 16) | + ((uint)d[6] << 8) | + d[7]; DadosLeitura.Pack.EstadoGeral = state; DadosLeitura.Pack.ChargeMosOn = chargeMos != 0; DadosLeitura.Pack.DischargeMosOn = dischgMos != 0; DadosLeitura.Pack.CiclosAproximados = lifeRaw; - DadosLeitura.Pack.CapacidadeRestante_Ah = remainmAh / 1000.0; + DadosLeitura.Pack.CapacidadeRestante_Ah = + remainmAh / 1000.0; } private void Atualizar094_StatusGeralIo(byte[] d) { - // 0: n series cells - // 1: n temps - // 2: charger conected - // 3: load connected - // 4: IO bits (0-3 DI, 4-7 DO) - byte nSeries = d[0]; - byte nTemps = d[1]; - byte charger = d[2]; - byte load = d[3]; - byte ioByte = d[4]; + byte nSeries = + d[0]; + + byte nTemps = + d[1]; + + byte charger = + d[2]; + + byte load = + d[3]; + + byte ioByte = + d[4]; DadosLeitura.Pack.NumeroCelulasSerie = nSeries; DadosLeitura.Pack.NumeroSensoresTemperatura = nTemps; - DadosLeitura.Pack.CarregadorConectado = (charger != 0); - DadosLeitura.Pack.LoadConectado = (load != 0); + DadosLeitura.Pack.CarregadorConectado = charger != 0; + DadosLeitura.Pack.LoadConectado = load != 0; DadosLeitura.Pack.IoStatusRaw = ioByte; } private void Atualizar095_TensoesCelulaFrame(byte[] d) { - // d[0] = frame index - // d[1..2], [3..4], [5..6] = até 3 tensões de célula (mV) - byte frame = d[0]; + byte frame = + d[0]; + + if (frame == 0xFF) + return; + + int totalCelulas = + DadosLeitura.Pack.NumeroCelulasSerie; for (int i = 0; i < 3; i++) { - int hiIndex = 1 + 2 * i; - int loIndex = hiIndex + 1; + int hiIndex = + 1 + 2 * i; + + int loIndex = + hiIndex + 1; + if (loIndex >= d.Length) break; - ushort raw = DecodeUInt16BE(d[hiIndex], d[loIndex]); + ushort raw = + DecodeUInt16BE( + d[hiIndex], + d[loIndex] + ); + if (raw == 0 || raw == 0xFFFF) continue; - // índice da célula é frame*3 + i + 1 (1-based) - int cellIndex = frame * 3 + i + 1; - double v = raw / 1000.0; + int cellIndex = + frame * 3 + i + 1; + + if (totalCelulas > 0 && + cellIndex > totalCelulas) + { + continue; + } + + double v = + raw / 1000.0; + + var cell = + DadosLeitura + .Celulas + .FirstOrDefault( + c => c.Index == cellIndex + ); - var cell = DadosLeitura.Celulas.FirstOrDefault(c => c.Index == cellIndex); if (cell == null) { - cell = new DalyCellVoltage - { - Index = cellIndex - }; + cell = + new DalyCellVoltage + { + Index = cellIndex + }; + DadosLeitura.Celulas.Add(cell); } @@ -356,26 +769,52 @@ namespace AgroBase.Services private void Atualizar096_TemperaturasFrame(byte[] d) { - // d[0] = frame index - // d[1..7] = até 7 sensores (raw = temp + 40, 0xFF=inválido) - byte frame = d[0]; + byte frame = + d[0]; + + if (frame == 0xFF) + return; + + int totalTemps = + DadosLeitura + .Pack + .NumeroSensoresTemperatura; for (int i = 1; i < 8 && i < d.Length; i++) { - byte raw = d[i]; + byte raw = + d[i]; + if (raw == 0xFF) continue; - int tempC = raw - 40; - int sensorIndex = frame * 7 + (i - 1) + 1; + int sensorIndex = + frame * 7 + (i - 1) + 1; + + if (totalTemps > 0 && + sensorIndex > totalTemps) + { + continue; + } + + int tempC = + raw - 40; + + var sensor = + DadosLeitura + .Temperaturas + .FirstOrDefault( + t => t.Index == sensorIndex + ); - var sensor = DadosLeitura.Temperaturas.FirstOrDefault(t => t.Index == sensorIndex); if (sensor == null) { - sensor = new DalyTemperatureSensor - { - Index = sensorIndex - }; + sensor = + new DalyTemperatureSensor + { + Index = sensorIndex + }; + DadosLeitura.Temperaturas.Add(sensor); } @@ -385,38 +824,53 @@ namespace AgroBase.Services private void Atualizar097_Balanceamento(byte[] d) { - // 6 bytes (ou 8) de bits: cada bit = 1 célula (até 48 células) - // Ex: bit0 do byte0 => célula1, bit1=>célula2, ... - var map = new Dictionary(); + var map = + new Dictionary(); - for (int byteIndex = 0; byteIndex < d.Length; byteIndex++) + for (int byteIndex = 0; + byteIndex < d.Length; + byteIndex++) { - byte b = d[byteIndex]; + byte b = + d[byteIndex]; + for (int bit = 0; bit < 8; bit++) { - int globalBit = byteIndex * 8 + bit; - if (globalBit >= 48) continue; + int globalBit = + byteIndex * 8 + bit; - bool ativo = ((b >> bit) & 0x01) == 1; - int cellIndex = globalBit + 1; + if (globalBit >= 48) + continue; + + bool ativo = + ((b >> bit) & 0x01) == 1; + + int cellIndex = + globalBit + 1; map[cellIndex] = ativo; } } - DadosLeitura.Balanceamento.CelulasEmBalanceamento = map; + DadosLeitura + .Balanceamento + .CelulasEmBalanceamento = map; - // Atualiza flag direto nas células foreach (var c in DadosLeitura.Celulas) { - c.EmBalanceamento = map.TryGetValue(c.Index, out bool ativo) && ativo; + c.EmBalanceamento = + map.TryGetValue( + c.Index, + out bool ativo + ) && + ativo; } } private void Atualizar098_Alarmes(byte[] d) { - // d[0..6] = bits de alarme, d[7] = fault code - var mensagens = new List(); + var mensagens = + new List(); void AddIf(byte b, int bit, string msg) { @@ -486,17 +940,14 @@ namespace AgroBase.Services AddIf(b6, 2, "Short circuit protect fault"); AddIf(b6, 3, "Low volt forbidden chg fault"); - byte faultCode = d[7]; + byte faultCode = + d[7]; DadosLeitura.Alarmes.Mensagens = mensagens; DadosLeitura.Alarmes.FaultCode = faultCode; - DadosLeitura.Alarmes.AlarmBytesRaw = d.Take(7).ToArray(); + DadosLeitura.Alarmes.AlarmBytesRaw = + d.Take(7).ToArray(); } - - - } } - - } diff --git a/AgroBase/AgroBase/Services/HerbicideCounterService.cs b/AgroBase/AgroBase/Services/HerbicideCounterService.cs index 4611bb0f8..d536259aa 100644 --- a/AgroBase/AgroBase/Services/HerbicideCounterService.cs +++ b/AgroBase/AgroBase/Services/HerbicideCounterService.cs @@ -144,14 +144,6 @@ namespace AgroBase.Services public void IniciarOperacao(double distanciaTotalMetrosAtual, double tempoTotalSegundosAtual) { - /* - * Antes, se o contador ainda não tivesse recebido uma leitura - * válida do reservatório, o início era ignorado. - * - * Isso fazia VolumeConsumidoOperacao_L ficar zero durante toda - * a operação quando a primeira leitura válida chegava depois - * do comando de iniciar. - */ if (!_inicializado) { OperacaoAtiva = true; @@ -229,19 +221,24 @@ namespace AgroBase.Services if (!_inicializado) { - Inicializar(distanciaTotalMetrosAtual, tempoTotalSegundosAtual); + Inicializar( + distanciaTotalMetrosAtual, + tempoTotalSegundosAtual + ); if (OperacaoAtiva || _inicioOperacaoPendente) { /* - * Primeira leitura válida depois do comando de iniciar. - * A operação passa a usar o volume atual como referência - * inicial, sem perder o estado de OperacaoAtiva. + * Primeira leitura válida depois do início da operação. + * Usa o volume atual como baseline da operação para começar + * PercentualConsumidoOperacao em zero. */ _volumeInicialOperacao_L = VolumeAtual_L; - _distanciaTotalAnterior_m = distanciaTotalMetrosAtual; - _tempoTotalAnterior_s = tempoTotalSegundosAtual; + _distanciaTotalAnterior_m = + distanciaTotalMetrosAtual; + _tempoTotalAnterior_s = + tempoTotalSegundosAtual; OperacaoAtiva = true; OperacaoFinalizada = false; @@ -306,15 +303,24 @@ namespace AgroBase.Services private double ObterVazaoFluxo_mLs() { - var dados = Variaveis.OperacaoEmAndamento.DispAtu?.Dados; + var dados = + Variaveis + .OperacaoEmAndamento + .DispAtu? + .Dados; if (dados == null) return 0; - double vazao = dados.VazaoFluxoMLpSReferencia; + double vazao = + dados.VazaoFluxoMLpSReferencia; - if (vazao < 0) + if (double.IsNaN(vazao) || + double.IsInfinity(vazao) || + vazao < 0) + { return 0; + } return vazao; } @@ -399,11 +405,15 @@ namespace AgroBase.Services ?? 0; if (qtdAtuacoes > 0) + { HerbicidaPorAtuacao_L = VolumeConsumidoOperacao_L / qtdAtuacoes; + } else + { HerbicidaPorAtuacao_L = 0; + } } private void AtualizarAutonomia(double velocidadeAtual_m_s) diff --git a/AgroBase/AgroBase/Services/OIDCanService.cs b/AgroBase/AgroBase/Services/OIDCanService.cs index 857688a7e..c87b1cc5c 100644 --- a/AgroBase/AgroBase/Services/OIDCanService.cs +++ b/AgroBase/AgroBase/Services/OIDCanService.cs @@ -1,4 +1,4 @@ -using AgroBase.Models; +using AgroBase.Models; using AgroBase.Models.Modules; using System; using System.Collections.Generic; @@ -11,248 +11,953 @@ namespace AgroBase.Services { public class OIDCanService { - public static List DadosLeitura = new List(); - private static T_Code Dispositivo { get; set; } = T_Code.Oid; - private static OidHandler _OidHandler { get; set; } = new OidHandler(); + public static List DadosLeitura = + new List(); + + private static T_Code Dispositivo { get; set; } = + T_Code.Oid; + + private static OidHandler _OidHandler { get; set; } = + new OidHandler(); + private static AsyncTaskTimerModel tmrHeartbeat; + private static readonly object _sync = + new object(); + + private static readonly HashSet _handlersRegistrados = + new HashSet(); + + private static readonly Dictionary + _ultimoEnvioConsultaPorDriver = + new Dictionary(); + + private static readonly Dictionary + _ultimoEnvioParametro = + new Dictionary(); + + private static readonly Dictionary + _ultimaConsultaPendente = + new Dictionary(); + + private static readonly Dictionary + _cursorPollingPorDriver = + new Dictionary(); + + private static readonly Dictionary + _ultimoComandoControleAssinatura = + new Dictionary(); + + private static readonly Dictionary + _ultimoComandoControleQuando = + new Dictionary(); + + /* + * Consultas síncronas do OID respondem sempre com DATA[0]=0x0F. + * A diferença fica em DATA[1]. Para evitar confusão no CanService, + * enviaremos no máximo uma consulta pendente por driver. + */ + private static readonly TimeSpan IntervaloMinimoConsultaPorDriver = + TimeSpan.FromMilliseconds(120); + + private static readonly TimeSpan IntervaloMinimoMesmoParametro = + TimeSpan.FromMilliseconds(300); + + private static readonly TimeSpan JanelaConsultaPendente = + TimeSpan.FromMilliseconds(450); + + private static readonly TimeSpan JanelaDedupeComandoControle = + TimeSpan.FromMilliseconds(80); + + private static readonly TimeSpan TempoRespostaRecente = + TimeSpan.FromSeconds(3); + + private static readonly TimeSpan TempoRemoverDriver = + TimeSpan.FromSeconds(15); + + /* + * O manual sugere heartbeat com período metade do timeout. + * O timer continua em 500ms, mas o payload passa a seguir o manual: + * DATA[0] = 0x00. + */ + private static readonly TimeSpan PeriodoHeartbeat = + TimeSpan.FromMilliseconds(500); + + /* + * Polling recomendado. Se outro ponto do sistema ainda chamar + * RequisitarDado() em massa, o throttle interno também segura. + */ + private static readonly OidParametros[] OrdemPolling = + { + OidParametros.VelocidadeAtual, + OidParametros.CorrenteMotor, + OidParametros.CorrenteBarramento, + OidParametros.CodErro, + OidParametros.Tensao, + OidParametros.Temperatura, + OidParametros.CicloTrabalho, + OidParametros.Potencia, + OidParametros.PosicaoAbsoluta, + OidParametros.IO_Status, + }; + + private static readonly Dictionary + IntervaloParametro = + new Dictionary + { + { OidParametros.VelocidadeAtual, TimeSpan.FromMilliseconds(160) }, + { OidParametros.CorrenteMotor, TimeSpan.FromMilliseconds(300) }, + { OidParametros.CorrenteBarramento, TimeSpan.FromMilliseconds(350) }, + { OidParametros.CodErro, TimeSpan.FromMilliseconds(750) }, + { OidParametros.Tensao, TimeSpan.FromSeconds(1) }, + { OidParametros.Temperatura, TimeSpan.FromSeconds(1) }, + { OidParametros.CicloTrabalho, TimeSpan.FromMilliseconds(500) }, + { OidParametros.Potencia, TimeSpan.FromMilliseconds(500) }, + { OidParametros.PosicaoAbsoluta, TimeSpan.FromMilliseconds(500) }, + { OidParametros.PosicaoRelativa, TimeSpan.FromMilliseconds(500) }, + { OidParametros.ModoCodificador, TimeSpan.FromSeconds(2) }, + { OidParametros.ErroLerCache, TimeSpan.FromSeconds(5) }, + { OidParametros.IO_Status, TimeSpan.FromSeconds(2) }, + }; + public static bool Iniciado { get { - return CanManager.CanService.IsConnected && SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Mov) && DadosLeitura.Any(x => x.Iniciado); + return + CanManager.CanService.IsConnected && + SerialService.DispositivosMapeados.Any( + x => x.Dispositivo == T_Code.Mov + ) && + DadosLeitura.Any( + x => x.Iniciado && + DateTime.Now - x.UltimoComandoRecebido <= + TempoRespostaRecente + ); } } + public static int TaxaAmostragem { - get - { - return 1000; - } + get { return 1000; } } public static async Task VerificaDispositivoConectado() { - if (!CanManager.CanService.IsConnected) return false; + if (!CanManager.CanService.IsConnected) + return false; - if (Variaveis.OperacaoEmAndamento.DispMvd == null) return false; + if (Variaveis.OperacaoEmAndamento.DispMvd == null) + return false; - List Mod_IDs = new List(); + List modIds = + new List(); - foreach (var Modulo in Variaveis.OperacaoEmAndamento.DispMvd.Dados.Modulos.Where(x => !x.MovMotor.Inicializado)) + var modulos = + Variaveis + .OperacaoEmAndamento + .DispMvd + .Dados + .Modulos + .Where(x => !x.MovMotor.Inicializado) + .ToList(); + + foreach (var modulo in modulos) { - CanManager.CanService.RegistrarHandler(Modulo.MovMotor._EnderecoCAN_Rx, _OidHandler); + byte idTx = + modulo.MovMotor._EnderecoCAN_Tx; - EnviarComandoDescoberta(Modulo.MovMotor._EnderecoCAN_Tx, Modulo.MovMotor._EnderecoCAN_Rx); + byte idRx = + modulo.MovMotor._EnderecoCAN_Rx; - DateTime Inicio = DateTime.Now; - bool Respondido() => DadosLeitura.Any(x => x.EnderecoRx == Modulo.MovMotor._EnderecoCAN_Rx && x.UltimoComandoRecebido > Inicio); - await FuncoesGlobais.AguardarCondicaoAsync(Respondido, 500, 50); + RegistrarHandler(idRx); + + EnviarComandoDescoberta(idTx, idRx); + + DateTime inicio = + DateTime.Now; + + bool Respondido() => + DadosLeitura.Any( + x => x.EnderecoRx == idRx && + x.UltimoComandoRecebido > inicio + ); + + await FuncoesGlobais.AguardarCondicaoAsync( + Respondido, + 800, + 50 + ); if (Respondido()) - { - Mod_IDs.Add(Modulo.Modulo_ID); - } + modIds.Add(modulo.Modulo_ID); } - if (Mod_IDs.Any()) - { - DefinirDispositivo(Mod_IDs); - } + if (modIds.Any()) + DefinirDispositivo(modIds); - return Mod_IDs.Any(); + return modIds.Any(); } - private static void DefinirDispositivo(List Mod_IDs) + private static void DefinirDispositivo(List modIds) { - if (tmrHeartbeat == null || !(tmrHeartbeat?.IsRunning ?? false)) + GarantirTimerHeartbeat(); + + foreach (var modId in modIds) { - tmrHeartbeat?.Dispose(); - tmrHeartbeat = new AsyncTaskTimerModel("tmrOidHeartbeat", tmrHeartbeat_Tick, 500); - tmrHeartbeat.Start(); - } + var modulo = + Variaveis + .OperacaoEmAndamento + .DispMvd + .Dados + .Modulos + .FirstOrDefault(x => x.Modulo_ID == modId)? + .MovMotor; - foreach (var Mod_ID in Mod_IDs) - { - var Modulo = Variaveis.OperacaoEmAndamento.DispMvd.Dados.Modulos.FirstOrDefault(x => x.Modulo_ID == Mod_ID).MovMotor; + if (modulo == null) + continue; - if (DadosLeitura.Any(x => x.EnderecoTx == Modulo._EnderecoCAN_Tx && !x.Configurado)) + RegistrarHandler(modulo._EnderecoCAN_Rx); + + var leitura = + DadosLeitura.FirstOrDefault( + x => x.EnderecoTx == modulo._EnderecoCAN_Tx + ); + + if (leitura != null && !leitura.Configurado) + ConfigurarDriver(modulo); + + if (!SerialService.DispositivosMapeados.Any( + x => x.Dispositivo == T_Code.Mov && + x.Mod_ID == modId)) { - ConfigurarDriver(Modulo); - } - - if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Mov && x.Mod_ID == Mod_ID)) - { - var Leitura = DadosLeitura.FirstOrDefault(x => x.EnderecoTx == Modulo._EnderecoCAN_Tx); - if (Leitura != null) - { - SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel() + SerialService.DispositivosMapeados.Add( + new DispositivoDetalhesModel { Dispositivo = T_Code.Mov, Endereco = CanManager.CanService._portName, Versao = "1.0", - Mod_ID = Mod_ID, - }); + Mod_ID = modId, + } + ); - Variaveis.OperacaoEmAndamento.ModsCalibragem.TryGetValue(T_Code.Mov, out var m); - if (m == null) - Variaveis.OperacaoEmAndamento.ModsCalibragem.Add(T_Code.Mov, new Dictionary()); - Variaveis.OperacaoEmAndamento.ModsCalibragem[T_Code.Mov][Mod_ID] = false; - Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog(T_Code.Mov, StatusModulo.Conectado, 0, "Dispositivo conectado", Mod_ID); + Variaveis.OperacaoEmAndamento.ModsCalibragem + .TryGetValue(T_Code.Mov, out var m); + + if (m == null) + { + Variaveis.OperacaoEmAndamento.ModsCalibragem + .Add( + T_Code.Mov, + new Dictionary() + ); } + + Variaveis.OperacaoEmAndamento + .ModsCalibragem[T_Code.Mov][modId] = false; + + Variaveis.OperacaoEmAndamento + .Sensoriamento + .InserirLog( + T_Code.Mov, + StatusModulo.Conectado, + 0, + "Dispositivo conectado", + modId + ); } } } + private static void GarantirTimerHeartbeat() + { + if (tmrHeartbeat != null && + (tmrHeartbeat.IsRunning)) + { + return; + } + + tmrHeartbeat?.Dispose(); + + tmrHeartbeat = + new AsyncTaskTimerModel( + "tmrOidHeartbeat", + tmrHeartbeat_Tick, + (int)PeriodoHeartbeat.TotalMilliseconds + ); + + tmrHeartbeat.Start(); + } + + private static void RegistrarHandler(byte idRx) + { + lock (_sync) + { + if (_handlersRegistrados.Contains(idRx)) + return; + + CanManager.CanService.RegistrarHandler( + idRx, + _OidHandler + ); + + _handlersRegistrados.Add(idRx); + } + } #region COMANDOS private static void EnviarComandoDescoberta(byte idTx, byte idRx) { - CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { idTx }, new byte[] { idRx }, (byte)OidFuncCode.Consulta, (byte)OidFuncCode.Consulta, new byte[] { (byte)OidParametros.CodErro }, true, false, true); + RegistrarHandler(idRx); + + CanManager.CanService.AdicionarMensagemNaFila( + Dispositivo, + new byte[] { idTx }, + new byte[] { idRx }, + (byte)OidFuncCode.Consulta, + (byte)OidFuncCode.Consulta, + new byte[] { (byte)OidParametros.CodErro }, + get: true, + contabilizarTimeoutHealth: false, + mensagemDescoberta: true + ); } - private static void EnviarComando(T_Code Dispositivo, byte idTx, byte idRx, byte funcCode, byte[] payload = null, bool get = true) + private static void EnviarComando( + T_Code dispositivo, + byte idTx, + byte idRx, + byte funcCode, + byte[] payload = null, + bool get = true, + bool contabilizarTimeoutHealth = true) { - CanManager.CanService.AdicionarMensagemNaFila(Dispositivo, new byte[] { idTx }, new byte[] { idRx }, funcCode, funcCode, payload, get); + RegistrarHandler(idRx); + + CanManager.CanService.AdicionarMensagemNaFila( + dispositivo, + new byte[] { idTx }, + new byte[] { idRx }, + funcCode, + funcCode, + payload ?? new byte[0], + get, + contabilizarTimeoutHealth + ); } - public static void RequisitarDado(byte idTx, byte idRx, OidParametros dado) + public static void RequisitarDado( + byte idTx, + byte idRx, + OidParametros dado) { - EnviarComando(Dispositivo, idTx, idRx, (byte)OidFuncCode.Consulta, new byte[] { (byte)dado }, true); + RequisitarDadoInterno( + idTx, + idRx, + dado, + forcar: false, + contabilizarTimeoutHealth: + DeveContabilizarTimeoutHealth(dado) + ); } - public static void ComandoControle(byte idTx, byte idRx, OidFuncCode funcCode, double setPoint) + private static bool RequisitarDadoInterno( + byte idTx, + byte idRx, + OidParametros dado, + bool forcar, + bool contabilizarTimeoutHealth) { - byte[] setPointBytes = null; + DateTime agora = + DateTime.Now; - switch (funcCode) + lock (_sync) { - case OidFuncCode.ComandoCorrente: - case OidFuncCode.AjusteCorrenteMaxima: - short corrente_10mA = (short)(setPoint * 100); - setPointBytes = BitConverter.GetBytes(corrente_10mA).Reverse().ToArray(); - DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx).ModoControle = OIDModoControle.Corrente; - break; - case OidFuncCode.ComandoCorrenteFreio: - { - short corrente_10mA_f = (short)(setPoint * 100); - setPointBytes = BitConverter.GetBytes(corrente_10mA_f).Reverse().ToArray(); - DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx).ModoControle = OIDModoControle.Freio; - break; - } - case OidFuncCode.ComandoVelocidade: - { - int erpm = (int)(setPoint * VariaveisEquipamento.NumeroPolosMotor); - setPointBytes = BitConverter.GetBytes(erpm).Reverse().ToArray(); - DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx).ModoControle = OIDModoControle.Velocidade; - break; - } - case OidFuncCode.ComandoCicloTrabalho: - { - short duty = (short)(Math.Max(-100, Math.Min(setPoint, 100)) * 10); - setPointBytes = BitConverter.GetBytes(duty).Reverse().ToArray(); - DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx).ModoControle = OIDModoControle.CicloTrabalho; - break; - } - case OidFuncCode.AjusteAceleracaoVelocidade: - case OidFuncCode.AjusteDesaceleracaoVelocidade: - { - if (setPoint <= 0) setPoint = 1; - double erpmMax = VariaveisEquipamento.RPM_Max_Roda * VariaveisEquipamento.RelacaoRPM * VariaveisEquipamento.NumeroPolosMotor; - int aceleracao = (int)(erpmMax / setPoint); // setPoint = tempo (s) + string chaveParametro = + idTx.ToString("X2") + + ":" + + ((byte)dado).ToString("X2"); - setPointBytes = BitConverter.GetBytes(aceleracao).Reverse().ToArray(); - break; - } - case OidFuncCode.Heartbeat: + if (!forcar) + { + if (_ultimoEnvioConsultaPorDriver + .TryGetValue(idTx, out DateTime ultimoDriver) && + agora - ultimoDriver < + IntervaloMinimoConsultaPorDriver) { - setPointBytes = new byte[] { (byte)setPoint }; - break; + return false; } - default: + + if (_ultimaConsultaPendente + .TryGetValue(idTx, out _) && + _ultimoEnvioConsultaPorDriver + .TryGetValue(idTx, out DateTime ultimoPendente) && + agora - ultimoPendente < + JanelaConsultaPendente) { - Console.WriteLine($"[OID] Comando nao mapeado: {funcCode.ToString()}"); - break; + return false; } + + TimeSpan intervalo = + IntervaloParametro.TryGetValue( + dado, + out TimeSpan valor + ) + ? valor + : IntervaloMinimoMesmoParametro; + + if (_ultimoEnvioParametro + .TryGetValue(chaveParametro, out DateTime ultimoParam) && + agora - ultimoParam < intervalo) + { + return false; + } + } + + _ultimoEnvioConsultaPorDriver[idTx] = agora; + _ultimoEnvioParametro[chaveParametro] = agora; + _ultimaConsultaPendente[idTx] = dado; } + EnviarComando( + Dispositivo, + idTx, + idRx, + (byte)OidFuncCode.Consulta, + new byte[] { (byte)dado }, + get: true, + contabilizarTimeoutHealth: + contabilizarTimeoutHealth + ); + + return true; + } + + private static bool DeveContabilizarTimeoutHealth( + OidParametros dado) + { + /* + * Saúde CAN deve refletir comandos/leituras críticas. + * Consultas secundárias podem falhar sem dizer que o driver caiu. + */ + return + dado == OidParametros.CodErro || + dado == OidParametros.VelocidadeAtual || + dado == OidParametros.CorrenteBarramento; + } + + public static void AtualizarDados() + { + try + { + var modulos = + Variaveis + .OperacaoEmAndamento + .DispMvd? + .Dados? + .Modulos; + + if (modulos == null) + return; + + foreach (var mod in modulos) + { + var motor = + mod?.MovMotor; + + if (motor == null || !motor.Inicializado) + continue; + + byte idTx = + motor._EnderecoCAN_Tx; + + byte idRx = + motor._EnderecoCAN_Rx; + + OidParametros? proximo = + EscolherProximoParametro(idTx); + + if (proximo.HasValue) + { + RequisitarDadoInterno( + idTx, + idRx, + proximo.Value, + forcar: false, + contabilizarTimeoutHealth: + DeveContabilizarTimeoutHealth(proximo.Value) + ); + } + } + + RemoverDriversMuitoAntigos(); + } + catch (Exception ex) + { + Console.WriteLine( + "[OID] Erro em AtualizarDados: " + + ex.Message + ); + } + } + + private static OidParametros? EscolherProximoParametro(byte idTx) + { + DateTime agora = + DateTime.Now; + + int cursor = + _cursorPollingPorDriver.TryGetValue( + idTx, + out int c + ) + ? c + : 0; + + for (int tentativa = 0; + tentativa < OrdemPolling.Length; + tentativa++) + { + int index = + (cursor + tentativa) % + OrdemPolling.Length; + + OidParametros parametro = + OrdemPolling[index]; + + string chave = + idTx.ToString("X2") + + ":" + + ((byte)parametro).ToString("X2"); + + TimeSpan intervalo = + IntervaloParametro.TryGetValue( + parametro, + out TimeSpan valor + ) + ? valor + : TimeSpan.FromMilliseconds(500); + + if (!_ultimoEnvioParametro.TryGetValue(chave, out DateTime ultimo) || + agora - ultimo >= intervalo) + { + _cursorPollingPorDriver[idTx] = + (index + 1) % + OrdemPolling.Length; + + return parametro; + } + } + + return null; + } + + private static void RemoverDriversMuitoAntigos() + { + DateTime agora = + DateTime.Now; + + List antigos; + + lock (DadosLeitura) + { + antigos = + DadosLeitura + .Where(x => + x.Iniciado && + x.UltimoComandoRecebido != DateTime.MinValue && + agora - x.UltimoComandoRecebido > + TempoRemoverDriver) + .ToList(); + } + + foreach (var antigo in antigos) + antigo.Iniciado = false; + } + + public static void ComandoControle( + byte idTx, + byte idRx, + OidFuncCode funcCode, + double setPoint) + { + byte[] setPointBytes = + MontarPayloadControle( + idTx, + funcCode, + setPoint + ); + if (setPointBytes == null) return; - if (funcCode != OidFuncCode.Heartbeat) + if (DeveDroparComandoDuplicado( + idTx, + funcCode, + setPointBytes)) { - //Console.WriteLine($"[OID] Comando enviado para fila: {idTx.ToString("X")} {((byte)funcCode).ToString("X")} {string.Join(" ", setPointBytes.Select(x => x.ToString("X")))}"); + return; } - EnviarComando(Dispositivo, idTx, idRx, (byte)funcCode, setPointBytes, false); + /* + * Controle não retorna resposta segundo o manual. + */ + EnviarComando( + Dispositivo, + idTx, + idRx, + (byte)funcCode, + setPointBytes, + get: false, + contabilizarTimeoutHealth: false + ); } - private static void ConfigurarDriver(MovimentacaoModel Modulo) + private static bool DeveDroparComandoDuplicado( + byte idTx, + OidFuncCode funcCode, + byte[] payload) { - byte addrTx = Modulo._EnderecoCAN_Tx; - byte addrRx = Modulo._EnderecoCAN_Rx; + if (funcCode == OidFuncCode.Heartbeat) + return false; - //ComandoControle(addrTx, addrRx, OidFuncCode.ComandoCorrenteFreio, 3.0); - ComandoControle(addrTx, addrRx, OidFuncCode.ComandoVelocidade, 0.0); - ComandoControle(addrTx, addrRx, OidFuncCode.AjusteCorrenteMaxima, Modulo.CorrenteTorqueMaximo); - ComandoControle(addrTx, addrRx, OidFuncCode.AjusteAceleracaoVelocidade, Modulo.Rampa); - ComandoControle(addrTx, addrRx, OidFuncCode.AjusteDesaceleracaoVelocidade, Modulo.Rampa); + string chave = + idTx.ToString("X2") + + ":" + + ((byte)funcCode).ToString("X2"); - DadosLeitura.Where(x => x.EnderecoTx == addrTx).First().Configurado = true; + string assinatura = + string.Join( + "", + payload.Select(x => x.ToString("X2")) + ); + + DateTime agora = + DateTime.Now; + + lock (_sync) + { + if (_ultimoComandoControleAssinatura + .TryGetValue(chave, out string anterior) && + anterior == assinatura && + _ultimoComandoControleQuando + .TryGetValue(chave, out DateTime quando) && + agora - quando < + JanelaDedupeComandoControle) + { + return true; + } + + _ultimoComandoControleAssinatura[chave] = assinatura; + _ultimoComandoControleQuando[chave] = agora; + return false; + } + } + + private static byte[] MontarPayloadControle( + byte idTx, + OidFuncCode funcCode, + double setPoint) + { + OIDModel item = + DadosLeitura.FirstOrDefault( + x => x.EnderecoTx == idTx + ); + + switch (funcCode) + { + case OidFuncCode.Heartbeat: + /* + * Manual: DATA[0] fixo em 0x00, DLC 1. + * Como o CanService já adiciona funcCode em DATA[0], + * payload vazio gera exatamente DATA = [0x00]. + */ + return new byte[0]; + + case OidFuncCode.ComandoCorrente: + { + short corrente_10mA = + (short)(setPoint * 100); + + item?.SetModoControleSeguro( + OIDModoControle.Corrente + ); + + return ToBigEndian(corrente_10mA); + } + + case OidFuncCode.AjusteCorrenteMaxima: + { + short corrente_10mA = + (short)(setPoint * 100); + + return ToBigEndian(corrente_10mA); + } + + case OidFuncCode.ComandoCorrenteFreio: + { + short corrente_10mA = + (short)(setPoint * 100); + + item?.SetModoControleSeguro( + OIDModoControle.Freio + ); + + return ToBigEndian(corrente_10mA); + } + + case OidFuncCode.ComandoVelocidade: + { + int erpm = + (int)( + setPoint * + VariaveisEquipamento.NumeroPolosMotor + ); + + item?.SetModoControleSeguro( + OIDModoControle.Velocidade + ); + + return ToBigEndian(erpm); + } + + case OidFuncCode.ComandoCicloTrabalho: + { + short duty = + (short)( + Math.Max( + -100, + Math.Min(setPoint, 100) + ) * 10 + ); + + item?.SetModoControleSeguro( + OIDModoControle.CicloTrabalho + ); + + return ToBigEndian(duty); + } + + case OidFuncCode.AjusteAceleracaoVelocidade: + case OidFuncCode.AjusteDesaceleracaoVelocidade: + { + if (setPoint <= 0) + setPoint = 1; + + double erpmMax = + VariaveisEquipamento.RPM_Max_Roda * + VariaveisEquipamento.RelacaoRPM * + VariaveisEquipamento.NumeroPolosMotor; + + int aceleracao = + (int)(erpmMax / setPoint); + + return ToBigEndian(aceleracao); + } + + default: + Console.WriteLine( + "[OID] Comando não mapeado: " + + funcCode + ); + return null; + } + } + + private static void ConfigurarDriver(MovimentacaoModel modulo) + { + if (modulo == null) + return; + + byte addrTx = + modulo._EnderecoCAN_Tx; + + byte addrRx = + modulo._EnderecoCAN_Rx; + + ComandoControle( + addrTx, + addrRx, + OidFuncCode.ComandoVelocidade, + 0.0 + ); + + ComandoControle( + addrTx, + addrRx, + OidFuncCode.AjusteCorrenteMaxima, + modulo.CorrenteTorqueMaximo + ); + + ComandoControle( + addrTx, + addrRx, + OidFuncCode.AjusteAceleracaoVelocidade, + modulo.Rampa + ); + + ComandoControle( + addrTx, + addrRx, + OidFuncCode.AjusteDesaceleracaoVelocidade, + modulo.Rampa + ); + + var item = + DadosLeitura.FirstOrDefault( + x => x.EnderecoTx == addrTx + ); + + if (item != null) + item.Configurado = true; } #endregion private static async Task tmrHeartbeat_Tick() { - foreach (var Mod in Variaveis.OperacaoEmAndamento.DispMvd?.Dados?.Modulos) + var modulos = + Variaveis + .OperacaoEmAndamento + .DispMvd? + .Dados? + .Modulos; + + if (modulos == null) + return; + + foreach (var mod in modulos) { - if (Mod.MovMotor.Inicializado) - { - int novoValor = Mod.MovMotor.heartBeatValue == 0 ? 1 : 0; + var motor = + mod?.MovMotor; - ComandoControle(Mod.MovMotor._EnderecoCAN_Tx, Mod.MovMotor._EnderecoCAN_Rx, OidFuncCode.Heartbeat, novoValor); + if (motor == null || !motor.Inicializado) + continue; - Mod.MovMotor.heartBeatValue = novoValor; - } + /* + * O driver exige heartbeat independente do modo. + */ + ComandoControle( + motor._EnderecoCAN_Tx, + motor._EnderecoCAN_Rx, + OidFuncCode.Heartbeat, + 0 + ); + + /* + * Mantém o campo antigo variando para UI/diagnóstico, + * mas ele não vira mais payload CAN. + */ + motor.heartBeatValue = + motor.heartBeatValue == 0 ? 1 : 0; } + + await Task.CompletedTask; + } + + private static byte[] ToBigEndian(short value) + { + byte[] b = + BitConverter.GetBytes(value); + + if (BitConverter.IsLittleEndian) + Array.Reverse(b); + + return b; + } + + private static byte[] ToBigEndian(int value) + { + byte[] b = + BitConverter.GetBytes(value); + + if (BitConverter.IsLittleEndian) + Array.Reverse(b); + + return b; + } + + private static short ReadInt16BE(byte[] data, int offset) + { + if (data == null || + data.Length < offset + 2) + { + return 0; + } + + return unchecked( + (short)( + (data[offset] << 8) | + data[offset + 1] + ) + ); + } + + private static ushort ReadUInt16BE(byte[] data, int offset) + { + if (data == null || + data.Length < offset + 2) + { + return 0; + } + + return (ushort)( + (data[offset] << 8) | + data[offset + 1] + ); + } + + private static int ReadInt32BE(byte[] data, int offset) + { + if (data == null || + data.Length < offset + 4) + { + return 0; + } + + return + (data[offset] << 24) | + (data[offset + 1] << 16) | + (data[offset + 2] << 8) | + data[offset + 3]; } public class OidHandler : ICanMessageHandler { - public T_Code Dispositivo { get; set; } = OIDCanService.Dispositivo; + public T_Code Dispositivo { get; set; } = + OIDCanService.Dispositivo; public enum OidFuncCode : byte { - Heartbeat = 0x00, - ComandoCorrente = 0x01, - ComandoVelocidade = 0x02, - ComandoCicloTrabalho = 0x03, - ComandoPosicaoAbsoluta = 0x04, - ComandoIncrementoPosicaoRelativaAnterior = 0x05, - ComandoIncrementoPosicaoRelativaAtual = 0x06, - ComandoPosicaoAtual = 0x07, - ComandoCorrenteFreio= 0x08, - ComandoCorrenteFreioMao = 0x09, - AjusteAceleracaoVelocidade = 0x0A, - AjusteVelocidadeMaxima = 0x0B, - AjusteAceleracaoeMaxima = 0x0C, - AjusteDesaceleracaoeMaxima = 0x0D, - AlterarTabelaConfiguracao = 0x0E, - Consulta = 0x0F, - AjusteDesaceleracaoVelocidade = 0x10, - ComandoRetornarZero = 0x11, - ComandoCancelarRetornoZero = 0x12, - ConsultaStatusRetornoZero = 0x13, - AjusteCorrenteMaxima = 0x14, - AjusteAceleracaoSubidaTorque = 0x15, - AjusteRampaTorque = 0x16, + Heartbeat = 0x00, + ComandoCorrente = 0x01, + ComandoVelocidade = 0x02, + ComandoCicloTrabalho = 0x03, + ComandoPosicaoAbsoluta = 0x04, + ComandoIncrementoPosicaoRelativaAnterior = 0x05, + ComandoIncrementoPosicaoRelativaAtual = 0x06, + ComandoPosicaoAtual = 0x07, + ComandoCorrenteFreio = 0x08, + ComandoCorrenteFreioMao = 0x09, + AjusteAceleracaoVelocidade = 0x0A, + AjusteVelocidadeMaxima = 0x0B, + AjusteAceleracaoeMaxima = 0x0C, + AjusteDesaceleracaoeMaxima = 0x0D, + AlterarTabelaConfiguracao = 0x0E, + Consulta = 0x0F, + AjusteDesaceleracaoVelocidade = 0x10, + ComandoRetornarZero = 0x11, + ComandoCancelarRetornoZero = 0x12, + ConsultaStatusRetornoZero = 0x13, + AjusteCorrenteMaxima = 0x14, + AjusteAceleracaoSubidaTorque = 0x15, + AjusteRampaTorque = 0x16, } public enum OidParametros : byte { - CodErro = 0, + CodErro = 0, VelocidadeAtual = 1, CicloTrabalho = 2, Potencia = 3, @@ -295,112 +1000,227 @@ namespace AgroBase.Services { lock (DadosLeitura) { - byte idTx = CanManager.ConverterIdCan(_idTx)[0]; - byte idRx = CanManager.ConverterIdCan(_idRx)[0]; - var item = DadosLeitura.FirstOrDefault(x => x.EnderecoTx == idTx && x.EnderecoRx == idRx); + byte idTx = + CanManager.ConverterIdCan(_idTx)[0]; + + byte idRx = + CanManager.ConverterIdCan(_idRx)[0]; + + var item = + DadosLeitura.FirstOrDefault( + x => x.EnderecoTx == idTx && + x.EnderecoRx == idRx + ); + if (item == null) { - item = new OIDModel { EnderecoTx = idTx, EnderecoRx = idRx }; + item = + new OIDModel + { + EnderecoTx = idTx, + EnderecoRx = idRx + }; + DadosLeitura.Add(item); } + return item; } } public void ProcessarMensagem(CanMessage mensagem) { - OidFuncCode func = (OidFuncCode)mensagem.DataRx[0]; - OidParametros parametro = (OidParametros)mensagem.DataRx[1]; - byte[] valorBytes = mensagem.DataRx.Skip(2).ToArray(); - - if(BitConverter.IsLittleEndian) - Array.Reverse(valorBytes); - - //Console.WriteLine($"[OID] Dados brutos recebidos do driver {mensagem.IdTx} - {FuncoesGlobais.ConverterComandoBytesParaTexto(mensagem.DataRx)}"); - - var item = ObterOuCriar(mensagem.IdTx, mensagem.IdRx); - try { - switch (parametro) + if (mensagem == null || + mensagem.DataRx == null || + mensagem.DataRx.Length < 2) { - case OidParametros.CodErro: - if (valorBytes.Length >= 2) - { - short codigo = BitConverter.ToInt16(valorBytes, 0); - item.CodigoAlarme.valor = (OidCodigoErro)codigo; - } - break; - - case OidParametros.VelocidadeAtual: - if (valorBytes.Length == 4) - item.ERPM.valor = BitConverter.ToInt32(valorBytes, 0); - break; - - case OidParametros.CicloTrabalho: - if (valorBytes.Length >= 2) - item.CicloTrabalho.valor = BitConverter.ToInt16(valorBytes, 0) / 10.0; // em % - break; - - case OidParametros.Potencia: - if (valorBytes.Length >= 2) - item.Potencia.valor = BitConverter.ToInt16(valorBytes, 0); // em Watts - break; - - case OidParametros.Tensao: - if (valorBytes.Length >= 2) - item.Tensao.valor = BitConverter.ToInt16(valorBytes, 0); // em Volts - break; - - case OidParametros.CorrenteMotor: - if (valorBytes.Length >= 2) - item.CorrenteMotor.valor = BitConverter.ToInt16(valorBytes, 0) / 100.0; // em A - break; - - case OidParametros.CorrenteBarramento: - if (valorBytes.Length >= 2) - item.CorrenteBarramento.valor = BitConverter.ToInt16(valorBytes, 0) / 100.0; // em A - break; - - case OidParametros.Temperatura: - if (valorBytes.Length >= 2) - item.Temperatura.valor = BitConverter.ToInt16(valorBytes, 0); // em °C - break; - - case OidParametros.PosicaoAbsoluta: - if (valorBytes.Length == 4) - item.PosicaoAbsoluta = BitConverter.ToInt32(valorBytes, 0) / 100.0; // em graus - break; - - case OidParametros.PosicaoRelativa: - if (valorBytes.Length == 4) - item.PosicaoRelativa = BitConverter.ToInt32(valorBytes, 0) / 100.0; // em graus - break; - - case OidParametros.ModoCodificador: - if (valorBytes.Length >= 2) - item.ModoCodificador = BitConverter.ToInt16(valorBytes, 0); // 0 ou 1 geralmente - break; - - case OidParametros.ErroLerCache: - if (valorBytes.Length >= 2) - item.ErroHistorico = BitConverter.ToInt16(valorBytes, 0); - break; - - case OidParametros.IO_Status: - if (valorBytes.Length >= 2) - item.IO_Status = BitConverter.ToUInt16(valorBytes, 0); // leitura binária - break; + return; } + OidFuncCode func = + (OidFuncCode)mensagem.DataRx[0]; + + if (func != OidFuncCode.Consulta && + func != OidFuncCode.ConsultaStatusRetornoZero) + { + /* + * Controle não deveria responder. Se responder algo + * diferente, não tenta interpretar como consulta. + */ + return; + } + + var item = + ObterOuCriar( + mensagem.IdTx, + mensagem.IdRx + ); + + if (func == OidFuncCode.ConsultaStatusRetornoZero) + { + ProcessarStatusRetornoZero( + item, + mensagem.DataRx + ); + + item.UltimoComandoRecebido = DateTime.Now; + item.Iniciado = true; + return; + } + + OidParametros parametro = + (OidParametros)mensagem.DataRx[1]; + + ProcessarParametro( + item, + parametro, + mensagem.DataRx + ); + item.UltimoComandoRecebido = DateTime.Now; + item.Iniciado = true; + + lock (_sync) + { + _ultimaConsultaPendente.Remove( + item.EnderecoTx + ); + } } catch (Exception ex) { - Console.WriteLine($"[OID] Erro ao interpretar resposta CAN ({parametro}): {ex.Message}"); + Console.WriteLine( + "[OID] Erro ao interpretar resposta CAN: " + + ex.Message + ); } } + + private void ProcessarParametro( + OIDModel item, + OidParametros parametro, + byte[] data) + { + switch (parametro) + { + case OidParametros.CodErro: + if (data.Length >= 4) + { + short codigo = + ReadInt16BE(data, 2); + + item.CodigoAlarme.valor = + (OidCodigoErro)codigo; + } + break; + + case OidParametros.VelocidadeAtual: + if (data.Length >= 6) + item.ERPM.valor = ReadInt32BE(data, 2); + break; + + case OidParametros.CicloTrabalho: + if (data.Length >= 4) + item.CicloTrabalho.valor = + ReadInt16BE(data, 2) / 10.0; + break; + + case OidParametros.Potencia: + if (data.Length >= 4) + item.Potencia.valor = + ReadInt16BE(data, 2); + break; + + case OidParametros.Tensao: + if (data.Length >= 4) + item.Tensao.valor = + ReadInt16BE(data, 2); + break; + + case OidParametros.CorrenteMotor: + if (data.Length >= 4) + item.CorrenteMotor.valor = + ReadInt16BE(data, 2) / 100.0; + break; + + case OidParametros.CorrenteBarramento: + if (data.Length >= 4) + item.CorrenteBarramento.valor = + ReadInt16BE(data, 2) / 100.0; + break; + + case OidParametros.Temperatura: + if (data.Length >= 4) + item.Temperatura.valor = + ReadInt16BE(data, 2); + break; + + case OidParametros.PosicaoAbsoluta: + if (data.Length >= 6) + item.PosicaoAbsoluta = + ReadInt32BE(data, 2) / 100.0; + break; + + case OidParametros.PosicaoRelativa: + if (data.Length >= 6) + item.PosicaoRelativa = + ReadInt32BE(data, 2) / 100.0; + break; + + case OidParametros.ModoCodificador: + /* + * Manual: resposta DLC 3, DATA[2]=0/1. + */ + if (data.Length >= 3) + item.ModoCodificador = data[2]; + break; + + case OidParametros.ErroLerCache: + /* + * Manual: DATA[2]~DATA[6] são erros em cache. + * Mantém o primeiro para compatibilidade. + */ + if (data.Length >= 3) + item.ErroHistorico = data[2]; + break; + + case OidParametros.IO_Status: + if (data.Length >= 4) + item.IO_Status = + ReadUInt16BE(data, 2); + break; + } + } + + private void ProcessarStatusRetornoZero( + OIDModel item, + byte[] data) + { + /* + * Manual: + * DATA[0] = 0x13 + * DATA[1] = 0x00 em retorno, 0x01 completo + * DATA[2] = 0 sucesso, -1 IO não configurado, -2 abortado + * + * O modelo atual pode não ter campo para isso. + * Mantém apenas UltimoComandoRecebido/Iniciado. + */ + } } } -} \ No newline at end of file + internal static class OidModelExtensions + { + public static void SetModoControleSeguro( + this OIDModel model, + OIDModoControle modo) + { + if (model == null) + return; + + model.ModoControle = modo; + } + } +}