diff --git a/AgroBase/AgroBase/AgroBase.csproj b/AgroBase/AgroBase/AgroBase.csproj
index 4272cbe06..c852c9229 100644
--- a/AgroBase/AgroBase/AgroBase.csproj
+++ b/AgroBase/AgroBase/AgroBase.csproj
@@ -575,6 +575,7 @@
+
diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs
index dee49133c..4f72b1a94 100644
--- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs
+++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs
@@ -2651,7 +2651,7 @@ namespace AgroBase.Models
public List ModulosSaude { get; set; }
public List DispositivosMapeados { get; set; }
public Dictionary> ModsCalibragem { get; set; }
- public bool CanSerialAtivado { get; set; }
+ public CanServiceTipo TipoCanAtivo { get; set; }
public List Logs { get; set; }
public DateTime UltimoRegistroLog { get; set; } = DateTime.MinValue;
public CoolerControlDataModel CoolerControl { get; set; }
@@ -2928,7 +2928,7 @@ namespace AgroBase.Models
DataFim = Variaveis.OperacaoEmAndamento.DataFim,
TempoAguardandoSeg = Variaveis.OperacaoEmAndamento.TempoAguardandoSegs,
TempoDecorridoSeg = Variaveis.OperacaoEmAndamento.TempoDecorridoSegs,
- CanSerialAtivado = CanManager.UseCanSerial,
+ TipoCanAtivo = CanManager.TipoServico,
IMU = dadosIMU,
Atuador = dadosAtuador,
Movimentacao = dadosMovimentacao,
@@ -2979,7 +2979,7 @@ namespace AgroBase.Models
DataFim = DataFim,
TempoAguardandoSeg = TempoAguardandoSeg,
TempoDecorridoSeg = TempoDecorridoSeg,
- CanSerialAtivado = CanSerialAtivado,
+ TipoCanAtivo = TipoCanAtivo,
Atuador = Atuador?.Clone(),
Movimentacao = Movimentacao?.Clone(),
Direcional = Direcional?.Clone(),
diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoParametrosModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoParametrosModel.cs
index 04931f4b2..8170ca09c 100644
--- a/AgroBase/AgroBase/Models/Operacoes/OperacaoParametrosModel.cs
+++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoParametrosModel.cs
@@ -504,7 +504,7 @@ namespace AgroBase.Models.Operacoes
{
public DateTime Momento { get; set; }
public StatusModulo StatusRover { get; set; }
- public bool CanSerial { get; set; }
+ public CanServiceTipo TipoCanAtivo { get; set; }
public OperacaoParametrosDadosOperacaoModel Operacao { get; set; }
public OperacaoParametrosDadosTrajetoriaModel Trajetoria { get; set; }
public OperacaoParametrosDadosControleModel Controle { get; set; }
@@ -537,7 +537,7 @@ namespace AgroBase.Models.Operacoes
Momento = DateTime.Now;
StatusRover = op.Status;
- CanSerial = CanManager.UseCanSerial;
+ TipoCanAtivo = CanManager.TipoServico;
Operacao = new OperacaoParametrosDadosOperacaoModel()
{
Iniciada = op.Iniciado,
@@ -806,7 +806,7 @@ namespace AgroBase.Models.Operacoes
{
Momento = Momento,
StatusRover = StatusRover,
- CanSerial = CanSerial,
+ TipoCanAtivo = TipoCanAtivo,
Operacao = Operacao,
Trajetoria = Trajetoria?.Clone(),
Controle = Controle?.Clone(),
diff --git a/AgroBase/AgroBase/Services/CANService.cs b/AgroBase/AgroBase/Services/CANService.cs
index a7da5278e..27c00fe25 100644
--- a/AgroBase/AgroBase/Services/CANService.cs
+++ b/AgroBase/AgroBase/Services/CANService.cs
@@ -4,24 +4,44 @@ using static AgroBase.Models.Enums;
namespace AgroBase.Services
{
+ public enum CanServiceTipo
+ {
+ Serial,
+ Waveshare,
+ Ethernet
+ }
+
public class CanManager
{
- public static bool UseCanSerial = true;
+ public static CanServiceTipo TipoServico = CanServiceTipo.Ethernet;
private static CanServiceWaveshare CanWaveshare = new CanServiceWaveshare();
private static CanServiceSerial CanSerial = new CanServiceSerial();
+ private static CanServiceEthernet CanEthernet = new CanServiceEthernet
+ {
+ IpAddress = "192.168.0.7",
+ Port = 8235,
+ TransportMode = CanEthernetTransportMode.Tcp
+ };
+
public static ICanService CanService
{
get
{
- if (UseCanSerial)
+ switch (TipoServico)
{
- return CanSerial;
- }
- else
- {
- return CanWaveshare;
+ case CanServiceTipo.Serial:
+ return CanSerial;
+
+ case CanServiceTipo.Waveshare:
+ return CanWaveshare;
+
+ case CanServiceTipo.Ethernet:
+ return CanEthernet;
+
+ default:
+ return CanSerial;
}
}
}
diff --git a/AgroBase/AgroBase/Services/CANServiceSerial.cs b/AgroBase/AgroBase/Services/CANServiceSerial.cs
index 1afd70e92..853394daf 100644
--- a/AgroBase/AgroBase/Services/CANServiceSerial.cs
+++ b/AgroBase/AgroBase/Services/CANServiceSerial.cs
@@ -237,6 +237,8 @@ namespace AgroBase.Services
tmrProcessarCAN.Start();
IsConnected = true;
+
+ DefinirDispositivo();
}
return IsConnected;
@@ -252,6 +254,23 @@ namespace AgroBase.Services
}
}
+ private void DefinirDispositivo()
+ {
+ if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Can))
+ {
+ if (IsConnected && Iniciado)
+ {
+ SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel()
+ {
+ Dispositivo = T_Code.Can,
+ Endereco = _portName,
+ Versao = ("1").ToString()
+ });
+ }
+ }
+ }
+
+
private async Task ProcessarCAN()
{
const int MAX_BATCH = 256; // teto de itens por tick
diff --git a/AgroBase/AgroBase/Services/CANServiceWaveshare.cs b/AgroBase/AgroBase/Services/CANServiceWaveshare.cs
index 4aaf93b34..6136c072e 100644
--- a/AgroBase/AgroBase/Services/CANServiceWaveshare.cs
+++ b/AgroBase/AgroBase/Services/CANServiceWaveshare.cs
@@ -131,6 +131,8 @@ namespace AgroBase.Services
else
{
MostrarLog($"Conversor CAN FD iniciado com sucesso no canal {Connector.Canal}.", 0);
+
+ DefinirDispositivo();
}
if (canIniciado || !IsConnected)
@@ -154,6 +156,23 @@ namespace AgroBase.Services
return canIniciado;
}
+ private void DefinirDispositivo()
+ {
+ if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Can))
+ {
+ if (IsConnected && Iniciado)
+ {
+ SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel()
+ {
+ Dispositivo = T_Code.Can,
+ Endereco = _portName,
+ Versao = ("1").ToString()
+ });
+ }
+ }
+ }
+
+
private async Task ProcessarCAN()
{
const int MAX_BATCH = 256; // teto de itens por tick
diff --git a/AgroBase/AgroBase/Services/CanServiceEthernet.cs b/AgroBase/AgroBase/Services/CanServiceEthernet.cs
new file mode 100644
index 000000000..0d8e404ed
--- /dev/null
+++ b/AgroBase/AgroBase/Services/CanServiceEthernet.cs
@@ -0,0 +1,715 @@
+using AgroBase.Models;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO.Ports;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using static AgroBase.Models.Enums;
+
+namespace AgroBase.Services
+{
+ public enum CanEthernetTransportMode
+ {
+ Tcp = 0,
+ Udp = 1
+ }
+
+ ///
+ /// Serviço CAN para conversores Ethernet-CAN no modo Standard Protocol Conversion do USR-CAN115.
+ /// Formato por frame: 13 bytes
+ /// [0] FrameInfo: bit7=Extended, bit6=RTR, bits3..0=DLC
+ /// [1..4] CAN ID big-endian
+ /// [5..12] Data CAN, sempre 8 bytes, preenchendo com 0x00 quando DLC menor.
+ ///
+ public class CanServiceEthernet : ICanService
+ {
+ public string _portName { get; set; } = "CAN-ETH";
+
+ public string IpAddress { get; set; } = "192.168.0.7";
+ public int Port { get; set; } = 8235;
+ public CanEthernetTransportMode TransportMode { get; set; } = CanEthernetTransportMode.Tcp;
+
+ private int _bitRate;
+ private TcpClient _tcpClient;
+ private NetworkStream _tcpStream;
+ private UdpClient _udpClient;
+ private IPEndPoint _remoteEndPoint;
+
+ private readonly object _socketLock = new object();
+ private readonly object _LockMensagens = new object();
+
+ private AsyncTaskTimerModel tmrOuvirCAN;
+ private AsyncTaskTimerModel tmrEnviarCAN;
+ private AsyncTaskTimerModel tmrProcessarCAN;
+
+ private readonly ConcurrentQueue<(uint id, byte[] data)> FramesRecebidos = new ConcurrentQueue<(uint, byte[])>();
+ private readonly List TcpRxBuffer = new List(4096);
+
+ private readonly ConcurrentDictionary _roteadores = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary _roteadoresRegistrados = new ConcurrentDictionary();
+ private readonly List MensagensPendentes = new List();
+
+ private uint? _ultimoIdEnviado = null;
+ private bool DebugMode = false;
+
+ public bool IsConnected { get; private set; }
+ public bool Iniciado { get; private set; }
+
+ public bool FilaLiberada
+ {
+ get
+ {
+ lock (_LockMensagens)
+ {
+ return MensagensPendentes == null ||
+ MensagensPendentes.Count == 0 ||
+ !MensagensPendentes.Any(x => !x.Enviado && x.Momento >= DateTime.Now.AddSeconds(-10));
+ }
+ }
+ }
+
+ public double UltimoTx { get; set; } = 0;
+ private DateTime _ultimoTx = DateTime.MinValue;
+ public double FreqTx { get; set; } = 0;
+
+ public double UltimoRx { get; set; } = 0;
+ private DateTime _ultimoRx = DateTime.MinValue;
+ public double FreqRx { get; set; } = 0;
+
+ public int ErrosCriticos { get; set; } = 0;
+ public double UltimoErroCritico { get; set; } = 0;
+ private DateTime _ultimoErroCritico = DateTime.MinValue;
+
+ private const int USR_FRAME_SIZE = 13;
+
+ private void MostrarLog(string message, uint id = 0)
+ {
+ if (DebugMode)
+ Console.WriteLine($"[CAN-ETH] {message}");
+ }
+
+ public bool PortaIsCan(SerialPort Porta = null)
+ {
+ return true;
+ }
+
+ public void RegistrarHandler(uint endereco_rx, ICanMessageHandler handler)
+ {
+ _roteadoresRegistrados.TryGetValue(endereco_rx, out bool registrado);
+ if (!registrado)
+ {
+ _roteadoresRegistrados.TryAdd(endereco_rx, true);
+ _roteadores.TryAdd(endereco_rx, handler);
+ MostrarLog($"Roteador registrado: 0x{endereco_rx:X}", endereco_rx);
+ }
+ }
+
+ public bool Inicializar(int BitRate = 250)
+ {
+ _bitRate = BitRate;
+
+ try
+ {
+ FecharConexaoSemLimparHandlers();
+
+ if (TransportMode == CanEthernetTransportMode.Tcp)
+ IsConnected = ConectarTcp();
+ else
+ IsConnected = ConectarUdp();
+
+ Iniciado = IsConnected;
+
+ if (!IsConnected)
+ return false;
+
+ _ultimoRx = DateTime.Now;
+ UltimoRx = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
+ _ultimoTx = DateTime.Now;
+ UltimoTx = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
+
+ tmrOuvirCAN?.Dispose();
+ tmrOuvirCAN = new AsyncTaskTimerModel("tmrOuvirCANEthernet", OuvirCAN, 1);
+ tmrOuvirCAN.Start();
+
+ tmrEnviarCAN?.Dispose();
+ tmrEnviarCAN = new AsyncTaskTimerModel("tmrEnviarCANEthernet", EnviarCAN, 3);
+ tmrEnviarCAN.Start();
+
+ tmrProcessarCAN?.Dispose();
+ tmrProcessarCAN = new AsyncTaskTimerModel("tmrProcessarCANEthernet", ProcessarCAN, 5);
+ tmrProcessarCAN.Start();
+
+ MostrarLog($"Inicializado via {TransportMode} em {IpAddress}:{Port}");
+
+ DefinirDispositivo();
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ MostrarLog($"Erro ao inicializar: {ex.Message}");
+ IsConnected = false;
+ Iniciado = false;
+ return false;
+ }
+ }
+
+ private bool ConectarTcp()
+ {
+ 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)
+ return false;
+
+ _tcpStream = _tcpClient.GetStream();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ MostrarLog($"Erro TCP: {ex.Message}");
+ return false;
+ }
+ }
+
+ private bool ConectarUdp()
+ {
+ try
+ {
+ _remoteEndPoint = new IPEndPoint(IPAddress.Parse(IpAddress), Port);
+ _udpClient = new UdpClient();
+ _udpClient.Client.ReceiveBufferSize = 64 * 1024;
+ _udpClient.Client.SendBufferSize = 64 * 1024;
+ _udpClient.Client.Blocking = false;
+ _udpClient.Connect(_remoteEndPoint);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ MostrarLog($"Erro UDP: {ex.Message}");
+ return false;
+ }
+ }
+
+ private void DefinirDispositivo()
+ {
+ if (!SerialService.DispositivosMapeados.Any(x => x.Dispositivo == T_Code.Can))
+ {
+ if (IsConnected && Iniciado)
+ {
+ SerialService.DispositivosMapeados.Add(new DispositivoDetalhesModel()
+ {
+ Dispositivo = T_Code.Can,
+ Endereco = IpAddress,
+ Versao = ("1").ToString()
+ });
+ }
+ }
+ }
+
+ public void AdicionarMensagemNaFila(T_Code Dispositivo, byte[] _idTx, byte[] _idRx, byte funcCodeTx, byte funcCodeRx, byte[] payload = null, bool get = true)
+ {
+ if (!IsConnected) return;
+
+ uint idTx = CanManager.ConverterIdCan(_idTx);
+ uint idRx = CanManager.ConverterIdCan(_idRx);
+
+ var dados = new List();
+
+ if (Dispositivo != T_Code.Bat)
+ dados.Add(funcCodeTx);
+
+ if (payload != null)
+ dados.AddRange(payload);
+
+ if (Dispositivo == T_Code.Mks)
+ {
+ byte crc = CanManager.CalcularChecksum(idTx, dados.ToArray());
+ dados.Add(crc);
+ }
+
+ if (dados.Count > 8)
+ throw new InvalidOperationException($"Payload CAN maior que 8 bytes. ID=0x{idTx:X}, Len={dados.Count}");
+
+ var novaMensagem = new CanMessage
+ {
+ Momento = DateTime.Now,
+ Dispositivo = Dispositivo,
+ IdTx = idTx,
+ IdRx = idRx,
+ funcCodeTx = funcCodeTx,
+ funcCodeRx = funcCodeRx,
+ DataTx = dados.ToArray(),
+ ComResposta = get,
+ ChaveInterna = CanManager.GerarChaveMensagem(Dispositivo, idRx, dados.ToArray())
+ };
+
+ 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()));
+
+ if (!mensagemNaFila)
+ {
+ MensagensPendentes.Add(novaMensagem);
+ MostrarLog($"Mensagem adicionada. ID=0x{idTx:X}, Data={BitConverter.ToString(novaMensagem.DataTx)}", idTx);
+ }
+ }
+ }
+
+ private CanMessage ObterProximaMensagem()
+ {
+ List mensagensPendentes;
+
+ lock (_LockMensagens)
+ {
+ mensagensPendentes = MensagensPendentes.Where(x => !x.Enviado).ToList();
+ }
+
+ if (!mensagensPendentes.Any())
+ return null;
+
+ var prioritarias = mensagensPendentes.Where(x => !x.ComResposta).ToList();
+ var baseLista = prioritarias.Any() ? prioritarias : mensagensPendentes;
+
+ var idsDisponiveis = baseLista.Select(x => x.IdTx).Distinct().OrderBy(x => x).ToList();
+
+ uint proximoId;
+ if (!_ultimoIdEnviado.HasValue || !idsDisponiveis.Contains(_ultimoIdEnviado.Value))
+ proximoId = idsDisponiveis.First();
+ else
+ {
+ int index = idsDisponiveis.IndexOf(_ultimoIdEnviado.Value);
+ proximoId = (index + 1 < idsDisponiveis.Count) ? idsDisponiveis[index + 1] : idsDisponiveis.First();
+ }
+
+ return baseLista.Where(x => x.IdTx == proximoId).OrderBy(x => x.Momento).FirstOrDefault();
+ }
+
+ private async Task EnviarCAN()
+ {
+ if (!IsConnected) return;
+
+ RefillTokens();
+
+ const int MAX_BURST = 8;
+ int sent = 0;
+
+ while (_tokens >= 1 && sent < MAX_BURST)
+ {
+ var msg = ObterProximaMensagem();
+ if (msg == null) break;
+
+ byte[] packet = CriarPacoteUsrStandard(msg);
+
+ if (!EnviarPacote(packet, msg.IdTx))
+ {
+ MostrarLog($"Erro ao enviar frame ID=0x{msg.IdTx:X}", msg.IdTx);
+ IsConnected = false;
+ break;
+ }
+
+ msg.Enviado = true;
+ msg.EnviadoEm = DateTime.Now;
+ _ultimoIdEnviado = msg.IdTx;
+ _tokens -= 1;
+ sent++;
+ }
+
+ if (sent == 0)
+ await Task.Delay(1);
+ }
+
+ private byte[] CriarPacoteUsrStandard(CanMessage msg)
+ {
+ bool isExtended = msg.IdTx > 0x7FF;
+ byte[] data = msg.DataTx ?? new byte[0];
+ int dlc = Math.Min(data.Length, 8);
+
+ byte frameInfo = (byte)(dlc & 0x0F);
+ if (isExtended)
+ frameInfo |= 0x80;
+
+ var packet = new byte[USR_FRAME_SIZE];
+ packet[0] = frameInfo;
+
+ packet[1] = (byte)((msg.IdTx >> 24) & 0xFF);
+ packet[2] = (byte)((msg.IdTx >> 16) & 0xFF);
+ packet[3] = (byte)((msg.IdTx >> 8) & 0xFF);
+ packet[4] = (byte)(msg.IdTx & 0xFF);
+
+ for (int i = 0; i < dlc; i++)
+ packet[5 + i] = data[i];
+
+ return packet;
+ }
+
+ private bool EnviarPacote(byte[] packet, uint id)
+ {
+ try
+ {
+ lock (_socketLock)
+ {
+ if (TransportMode == CanEthernetTransportMode.Tcp)
+ {
+ if (_tcpStream == null || !_tcpStream.CanWrite)
+ return false;
+
+ _tcpStream.Write(packet, 0, packet.Length);
+ _tcpStream.Flush();
+ }
+ else
+ {
+ if (_udpClient == null)
+ return false;
+
+ _udpClient.Send(packet, packet.Length);
+ }
+ }
+
+ var agora = DateTime.Now;
+ FreqTx = _ultimoTx == DateTime.MinValue ? 0 : 1.0 / Math.Max(0.001, (agora - _ultimoTx).TotalSeconds);
+ _ultimoTx = agora;
+ UltimoTx = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
+
+ MostrarLog($"TX ID=0x{id:X}, Packet={BitConverter.ToString(packet)}", id);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ MostrarLog($"Erro TX: {ex.Message}", id);
+ return false;
+ }
+ }
+
+ private async Task OuvirCAN()
+ {
+ if (!IsConnected) return;
+
+ try
+ {
+ if (TransportMode == CanEthernetTransportMode.Tcp)
+ OuvirTcp();
+ else
+ OuvirUdp();
+ }
+ catch (Exception ex)
+ {
+ MostrarLog($"Erro RX: {ex.Message}");
+ IsConnected = false;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ private void OuvirTcp()
+ {
+ if (_tcpClient == null || _tcpStream == null || !_tcpClient.Connected)
+ {
+ IsConnected = false;
+ return;
+ }
+
+ while (_tcpStream.DataAvailable)
+ {
+ byte[] buffer = new byte[4096];
+ int lidos = _tcpStream.Read(buffer, 0, buffer.Length);
+ 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)
+ {
+ 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)
+ {
+ byte[] frame = new byte[USR_FRAME_SIZE];
+ Buffer.BlockCopy(pacote, offset, frame, 0, USR_FRAME_SIZE);
+ ProcessarPacoteUsrStandard(frame);
+ offset += USR_FRAME_SIZE;
+ }
+ }
+ }
+
+ private void ProcessarPacoteUsrStandard(byte[] packet)
+ {
+ if (packet == null || packet.Length != USR_FRAME_SIZE)
+ return;
+
+ byte frameInfo = packet[0];
+ bool isExtended = (frameInfo & 0x80) != 0;
+ bool isRemote = (frameInfo & 0x40) != 0;
+ int dlc = frameInfo & 0x0F;
+
+ if (dlc < 0 || dlc > 8 || isRemote)
+ return;
+
+ uint id = ((uint)packet[1] << 24) |
+ ((uint)packet[2] << 16) |
+ ((uint)packet[3] << 8) |
+ packet[4];
+
+ if (!isExtended)
+ id &= 0x7FF;
+
+ byte[] data = new byte[dlc];
+ if (dlc > 0)
+ Buffer.BlockCopy(packet, 5, data, 0, dlc);
+
+ FramesRecebidos.Enqueue((id, data));
+ while (FramesRecebidos.Count > 4096)
+ FramesRecebidos.TryDequeue(out _);
+
+ var agora = DateTime.Now;
+ FreqRx = _ultimoRx == DateTime.MinValue ? 0 : 1.0 / Math.Max(0.001, (agora - _ultimoRx).TotalSeconds);
+ _ultimoRx = agora;
+ UltimoRx = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
+
+ MostrarLog($"RX {(isExtended ? "EXT" : "STD")} ID=0x{id:X}, Data={BitConverter.ToString(data)}", id);
+ }
+
+ private async Task ProcessarCAN()
+ {
+ const int MAX_BATCH = 256;
+ const int TIME_BUDGET_US = 1000;
+
+ var sw = Stopwatch.StartNew();
+ int processed = 0;
+
+ while (processed < MAX_BATCH && FramesRecebidos.TryDequeue(out var frame))
+ {
+ ProcessarFrameCAN(frame);
+ processed++;
+
+ if (sw.ElapsedTicks * (1_000_000.0 / Stopwatch.Frequency) > TIME_BUDGET_US)
+ break;
+ }
+
+ LimparMensagensAntigas();
+ VerificaSaudeConexao();
+
+ await Task.CompletedTask;
+ }
+
+ private void ProcessarFrameCAN((uint id, byte[] data) frame)
+ {
+ uint id = frame.id;
+ byte[] data = frame.data ?? new byte[0];
+
+ try
+ {
+ _roteadores.TryGetValue(id, out ICanMessageHandler handler);
+
+ T_Code dispositivo = handler?.Dispositivo ?? T_Code.Vzo;
+ bool db = dispositivo == T_Code.Bat;
+
+ byte fCodeRx = db ? (byte)DalyBMSService.GetDataId(id) : data.Length > 0 ? data[0] : (byte)0x00;
+ string chave = CanManager.GerarChaveMensagem(dispositivo, id, data);
+
+ CanMessage mensagem = null;
+
+ lock (_LockMensagens)
+ {
+ mensagem = MensagensPendentes.FirstOrDefault(x =>
+ x.Enviado &&
+ !x.Respondido &&
+ x.Dispositivo == dispositivo &&
+ x.IdRx == id &&
+ x.DataTx != null &&
+ (db || data.Length < 2 || x.DataTx.Length < 2 || x.DataTx[1] == data[1]));
+ }
+
+ if (mensagem == null)
+ {
+ mensagem = new CanMessage
+ {
+ Dispositivo = dispositivo,
+ IdRx = id,
+ funcCodeRx = fCodeRx,
+ DataRx = data,
+ ChaveInterna = chave
+ };
+ }
+
+ mensagem.DataRx = data;
+ mensagem.Respondido = true;
+ mensagem.RespondidoEm = DateTime.Now;
+
+ if (handler != null && dispositivo != T_Code.Vzo)
+ handler.ProcessarMensagem(mensagem);
+ else
+ MostrarLog($"Roteador não registrado. ID=0x{id:X}, Data={BitConverter.ToString(data)}", id);
+ }
+ catch (Exception ex)
+ {
+ MostrarLog($"Erro ao processar frame ID=0x{id:X}: {ex.Message}", id);
+ }
+ }
+
+ private void LimparMensagensAntigas()
+ {
+ var agora = DateTime.Now;
+
+ lock (_LockMensagens)
+ {
+ var remover = MensagensPendentes
+ .Where(x =>
+ (!x.ComResposta && x.Enviado) ||
+ (x.ComResposta && x.Respondido) ||
+ (x.ComResposta && x.Enviado && !x.Respondido && x.EnviadoEm < agora.AddSeconds(-2)) ||
+ (x.Momento < agora.AddSeconds(-30)))
+ .ToList();
+
+ foreach (var item in remover)
+ MensagensPendentes.Remove(item);
+ }
+ }
+
+ private void VerificaSaudeConexao()
+ {
+ DateTime agora = DateTime.Now;
+
+ bool socketOk = TransportMode == CanEthernetTransportMode.Tcp
+ ? (_tcpClient != null && _tcpClient.Connected && _tcpStream != null)
+ : (_udpClient != null);
+
+ if (!socketOk)
+ {
+ IsConnected = false;
+ return;
+ }
+
+ // UDP não tem conexão real. Este watchdog só reinicia se o sistema ficou tempo demais sem TX e RX.
+ if (IsConnected && ((agora - _ultimoTx).TotalSeconds > 2 && (agora - _ultimoRx).TotalSeconds > 2))
+ {
+ ErrosCriticos++;
+ _ultimoErroCritico = agora;
+ UltimoErroCritico = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
+ MostrarLog("Muito tempo sem tráfego CAN-ETH, reiniciando conexão...");
+ ReabrirConexao();
+ }
+
+ if ((agora - _ultimoErroCritico).TotalMinutes > 10)
+ ErrosCriticos = 0;
+ }
+
+ private void ReabrirConexao()
+ {
+ try
+ {
+ FecharConexaoSemLimparHandlers();
+ if (TransportMode == CanEthernetTransportMode.Tcp)
+ IsConnected = ConectarTcp();
+ else
+ IsConnected = ConectarUdp();
+
+ Iniciado = IsConnected;
+ _ultimoRx = DateTime.Now;
+ _ultimoTx = DateTime.Now;
+ }
+ catch
+ {
+ IsConnected = false;
+ Iniciado = false;
+ }
+ }
+
+ private void FecharConexaoSemLimparHandlers()
+ {
+ lock (_socketLock)
+ {
+ try { _tcpStream?.Close(); } catch { }
+ try { _tcpClient?.Close(); } catch { }
+ try { _udpClient?.Close(); } catch { }
+
+ _tcpStream = null;
+ _tcpClient = null;
+ _udpClient = null;
+ _remoteEndPoint = null;
+ TcpRxBuffer.Clear();
+ }
+
+ IsConnected = false;
+ Iniciado = false;
+ }
+
+ public void Fechar()
+ {
+ tmrOuvirCAN?.Dispose();
+ tmrEnviarCAN?.Dispose();
+ tmrProcessarCAN?.Dispose();
+
+ lock (_LockMensagens)
+ MensagensPendentes.Clear();
+
+ while (FramesRecebidos.TryDequeue(out _)) { }
+
+ _ultimoIdEnviado = null;
+ FecharConexaoSemLimparHandlers();
+ }
+
+ public void Dispose()
+ {
+ Fechar();
+ }
+
+ // alvo: ~200 fps, igual ao serviço USB atual
+ private const double TARGET_FPS = 200;
+ private double _tokens = TARGET_FPS;
+ private const double _capacity = TARGET_FPS;
+ private const double _refillPerMs = TARGET_FPS / 1000.0;
+ private DateTime _lastRefill = DateTime.UtcNow;
+
+ private void RefillTokens()
+ {
+ var now = DateTime.UtcNow;
+ var dtMs = (now - _lastRefill).TotalMilliseconds;
+ if (dtMs > 0)
+ {
+ _tokens = Math.Min(_capacity, _tokens + dtMs * _refillPerMs);
+ _lastRefill = now;
+ }
+ }
+ }
+}
diff --git a/AgroBase/AgroBase/Services/SerialService.cs b/AgroBase/AgroBase/Services/SerialService.cs
index 3cb2b6814..adb9b9f39 100644
--- a/AgroBase/AgroBase/Services/SerialService.cs
+++ b/AgroBase/AgroBase/Services/SerialService.cs
@@ -232,7 +232,7 @@ namespace AgroBase.Services
if (Variaveis.IsAgroMonitor) continue;
- if (CanManager.UseCanSerial && !CanManager.CanService.Iniciado)
+ if (CanManager.TipoServico == CanServiceTipo.Serial && !CanManager.CanService.Iniciado)
{
bool dispositivoCan = await ProcurarDispositivosCAN(_Porta);
if (dispositivoCan)