agrobot_base/AgroBase/AgroBase/Services/SerialService.cs

2472 lines
74 KiB
C#

using AgroBase.Forms;
using AgroBase.Models;
using AgroBase.Services.Operadores;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using static AgroBase.Models.Enums;
namespace AgroBase.Services
{
/// <summary>
/// Descoberta e supervisão das conexões físicas do rover.
///
/// Garantias:
/// - somente uma varredura executa por vez;
/// - shutdown cancela e aguarda a varredura atual;
/// - leituras com timeout não abandonam threads bloqueadas;
/// - escritas na mesma porta são serializadas;
/// - uma porta que não é CAN não remove dispositivos CAN válidos;
/// - rotinas de versionamento/sincronização não se acumulam;
/// - tempos internos usam Stopwatch;
/// - operações de interface nunca bloqueiam a varredura.
/// </summary>
public class SerialService
{
// ============================================================
// CONTRATO PÚBLICO LEGADO
// ============================================================
public static int InteraloVerificacao = 5000;
public static readonly string BeginLine = "@";
public static readonly string BreakLine = "&";
public static readonly string EndLine = "$";
public static readonly string BeginSensor = "#";
public static readonly string SplitMessage = ";";
public static readonly string SplitParams = ",";
public static readonly string SplitConfig = "*";
public static readonly string SplitSubParams = "~";
public static readonly List<T_Code> DispositivosMotores =
new List<T_Code> { T_Code.Mov, T_Code.Dir };
public static readonly List<T_Code> DispositivosConexaoInicial =
new List<T_Code> { T_Code.Mvd, T_Code.Atu, T_Code.Sen };
public static readonly List<T_Code> DispositivosProprios =
new List<T_Code> { T_Code.Atu, T_Code.Sen };
public static readonly List<T_Code> DispositivosModbus =
new List<T_Code> { T_Code.Pzm, T_Code.Wit, T_Code.A05 };
public static readonly List<T_Code> DispositivosCan =
new List<T_Code>
{
T_Code.Mov,
T_Code.Oid,
T_Code.Dir,
T_Code.Mks,
T_Code.Sen,
T_Code.Atu
};
public static readonly List<T_Code> DispositivosMultiplos =
new List<T_Code>
{
T_Code.Bld,
T_Code.Mks,
T_Code.Mvd,
T_Code.Oid,
T_Code.Mov,
T_Code.Dir
};
/// <summary>
/// Mantido público por compatibilidade.
/// Código novo deve preferir GetDispositivosMapeadosSnapshot(),
/// AdicionarOuAtualizarDispositivoMapeado() e RemoverDispositivoMapeado().
/// </summary>
public static List<DispositivoDetalhesModel> DispositivosMapeados =
CriarListaInicialDispositivos();
// ============================================================
// SINCRONIZAÇÃO E CICLO DE VIDA
// ============================================================
private static readonly SemaphoreSlim _scanGate =
new SemaphoreSlim(1, 1);
private static readonly SemaphoreSlim _updateDevicesGate =
new SemaphoreSlim(1, 1);
private static readonly SemaphoreSlim _versioningGate =
new SemaphoreSlim(1, 1);
private static readonly SemaphoreSlim _syncDataGate =
new SemaphoreSlim(1, 1);
private static readonly object _mappedDevicesLock =
new object();
private static readonly object _connectedDevicesLock =
new object();
private static readonly object _lifecycleLock =
new object();
private static readonly object _metricsTextLock =
new object();
private static readonly ConcurrentDictionary<
string,
SemaphoreSlim> _portIoLocks =
new ConcurrentDictionary<
string,
SemaphoreSlim>(
StringComparer.OrdinalIgnoreCase
);
private static readonly ConcurrentDictionary<
string,
int> _missingPortConfirmations =
new ConcurrentDictionary<
string,
int>(
StringComparer.OrdinalIgnoreCase
);
public static int ConfirmacoesPortaAusente { get; set; } = 2;
private static CancellationTokenSource _lifetimeCts =
new CancellationTokenSource();
private static CancellationTokenSource _currentScanCts;
private static long _scanGeneration;
private static int _isScanning;
private static int _isClosing;
// ============================================================
// MANUTENÇÃO
// ============================================================
public static int IntervaloVersionamentoMs { get; set; } = 60000;
public static int IntervaloSincronizacaoMs { get; set; } = 60000;
private static long _lastVersioningStartMono;
private static long _lastSyncStartMono;
// ============================================================
// MÉTRICAS
// ============================================================
private static long _scanStarted;
private static long _scanCompleted;
private static long _scanSkipped;
private static long _scanCanceled;
private static long _scanErrors;
private static long _portsEnumerated;
private static long _portsProbed;
private static long _portsBusy;
private static long _portsProbeErrors;
private static long _gpsFound;
private static long _loraFound;
private static long _canAdapterFound;
private static long _devicesRemoved;
private static long _serialWriteAttempts;
private static long _serialWriteSuccesses;
private static long _serialWriteTimeouts;
private static long _serialWriteErrors;
private static long _lastScanStartMono;
private static long _lastScanEndMono;
private static double _lastScanDurationMs;
private static double _maxScanDurationMs;
private static long _versioningRuns;
private static long _versioningErrors;
private static long _syncRuns;
private static long _syncErrors;
private static string _lastError;
private static int _uiUpdateScheduled;
private static string _pendingUiMessage;
// ============================================================
// ESCRITA SERIAL
// ============================================================
public static bool EnviarDadosPortaSerial(
SerialPort porta,
string dados,
bool DebugMode = false,
int timeout = 2000)
{
if (string.IsNullOrEmpty(dados))
{
if (DebugMode)
Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] Nenhum dado para enviar.");
return false;
}
byte[] bytes = Encoding.ASCII.GetBytes(dados);
return EnviarDadosPortaSerial(
porta,
bytes,
0,
bytes.Length,
DebugMode,
timeout
);
}
public static bool EnviarDadosPortaSerial(
SerialPort porta,
byte[] dados,
int inicio,
int comprimento,
bool DebugMode = false)
{
return EnviarDadosPortaSerial(
porta,
dados,
inicio,
comprimento,
DebugMode,
2000
);
}
private static bool EnviarDadosPortaSerial(
SerialPort porta,
byte[] dados,
int inicio,
int comprimento,
bool debugMode,
int timeout)
{
Interlocked.Increment(ref _serialWriteAttempts);
if (!ValidarFaixaBuffer(
dados,
inicio,
comprimento))
{
if (debugMode)
Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] Buffer serial inválido.");
Interlocked.Increment(ref _serialWriteErrors);
return false;
}
if (porta == null)
{
if (debugMode)
Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] A porta serial não está definida.");
Interlocked.Increment(ref _serialWriteErrors);
return false;
}
SemaphoreSlim gate = GetPortIoLock(porta);
bool acquired = false;
try
{
acquired = gate.Wait(
Math.Max(1, timeout)
);
if (!acquired)
{
Interlocked.Increment(
ref _serialWriteTimeouts
);
if (debugMode)
{
Variaveis.MostrarLog(
"[SerialService.EnviarDadosPortaSerial] Timeout aguardando acesso exclusivo à porta."
);
}
return false;
}
if (!porta.IsOpen)
{
if (debugMode)
Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] A porta serial não está aberta.");
Interlocked.Increment(ref _serialWriteErrors);
return false;
}
int oldTimeout = porta.WriteTimeout;
try
{
porta.WriteTimeout = Math.Max(1, timeout);
porta.Write(dados, inicio, comprimento);
}
finally
{
try { porta.WriteTimeout = oldTimeout; }
catch { }
}
Interlocked.Increment(
ref _serialWriteSuccesses
);
if (debugMode)
Variaveis.MostrarLog("[SerialService.EnviarDadosPortaSerial] Dados enviados com sucesso.");
return true;
}
catch (TimeoutException ex)
{
Interlocked.Increment(
ref _serialWriteTimeouts
);
RecordError(
"[SerialService.Write] Timeout em " +
SafePortName(porta) + ": " + ex.Message
);
return false;
}
catch (InvalidOperationException ex)
{
Interlocked.Increment(ref _serialWriteErrors);
RecordError(
"[SerialService.Write] Porta indisponível " +
SafePortName(porta) + ": " + ex.Message
);
return false;
}
catch (UnauthorizedAccessException ex)
{
Interlocked.Increment(ref _serialWriteErrors);
RecordError(
"[SerialService.Write] Acesso negado em " +
SafePortName(porta) + ": " + ex.Message
);
return false;
}
catch (IOException ex)
{
Interlocked.Increment(ref _serialWriteErrors);
RecordError(
"[SerialService.Write] Erro de I/O em " +
SafePortName(porta) + ": " + ex.Message
);
return false;
}
catch (Exception ex)
{
Interlocked.Increment(ref _serialWriteErrors);
RecordError(
"[SerialService.Write] Erro inesperado em " +
SafePortName(porta) + ": " + ex.Message
);
return false;
}
finally
{
if (acquired)
gate.Release();
}
}
public static async Task<bool> EnviarDadosPortaSerialAsync(
SerialPort porta,
byte[] dados,
int inicio,
int comprimento,
int timeout = 2000,
CancellationToken cancellationToken =
default(CancellationToken))
{
Interlocked.Increment(ref _serialWriteAttempts);
if (!ValidarFaixaBuffer(
dados,
inicio,
comprimento) ||
porta == null)
{
Interlocked.Increment(ref _serialWriteErrors);
return false;
}
SemaphoreSlim gate = GetPortIoLock(porta);
using (var timeoutCts =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken))
{
timeoutCts.CancelAfter(
Math.Max(1, timeout)
);
bool acquired = false;
try
{
await gate.WaitAsync(timeoutCts.Token)
.ConfigureAwait(false);
acquired = true;
if (!porta.IsOpen)
{
Interlocked.Increment(
ref _serialWriteErrors
);
return false;
}
/*
* SerialPort.Write é síncrono. O lock e o WriteTimeout
* impedem sobreposição e limitam o bloqueio no driver.
* Não usamos Task.Run, pois ele abandonaria uma thread
* caso o driver ignorasse cancelamento.
*/
int oldTimeout = porta.WriteTimeout;
try
{
porta.WriteTimeout =
Math.Max(1, timeout);
porta.Write(
dados,
inicio,
comprimento
);
}
finally
{
try { porta.WriteTimeout = oldTimeout; }
catch { }
}
Interlocked.Increment(
ref _serialWriteSuccesses
);
return true;
}
catch (OperationCanceledException)
{
Interlocked.Increment(
ref _serialWriteTimeouts
);
return false;
}
catch (TimeoutException)
{
Interlocked.Increment(
ref _serialWriteTimeouts
);
return false;
}
catch (Exception ex)
{
Interlocked.Increment(
ref _serialWriteErrors
);
RecordError(
"[SerialService.WriteAsync] " +
SafePortName(porta) + ": " + ex.Message
);
return false;
}
finally
{
if (acquired)
gate.Release();
}
}
}
// ============================================================
// VARREDURA PRINCIPAL
// ============================================================
public static Task RealizarVarreduraPortasUSB()
{
return RealizarVarreduraPortasUSB(
CancellationToken.None
);
}
public static async Task RealizarVarreduraPortasUSB(
CancellationToken cancellationToken)
{
if (Variaveis.Fechando ||
Volatile.Read(ref _isClosing) == 1)
{
return;
}
EnsureLifetimeAvailable();
CancellationToken lifetimeToken;
lock (_lifecycleLock)
lifetimeToken = _lifetimeCts.Token;
using (var linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
lifetimeToken))
{
bool entered;
try
{
entered = await _scanGate
.WaitAsync(0, linkedCts.Token)
.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
if (!entered)
{
Interlocked.Increment(ref _scanSkipped);
return;
}
Interlocked.Exchange(ref _isScanning, 1);
Interlocked.Increment(ref _scanStarted);
long generation =
Interlocked.Increment(
ref _scanGeneration
);
long startMono = Stopwatch.GetTimestamp();
Interlocked.Exchange(
ref _lastScanStartMono,
startMono
);
lock (_lifecycleLock)
_currentScanCts = linkedCts;
try
{
await ExecutarVarreduraAsync(
generation,
linkedCts.Token
).ConfigureAwait(false);
Interlocked.Increment(ref _scanCompleted);
}
catch (OperationCanceledException)
{
Interlocked.Increment(ref _scanCanceled);
}
catch (Exception ex)
{
Interlocked.Increment(ref _scanErrors);
RecordError(
"[SerialService.Scan] " + ex
);
}
finally
{
long endMono = Stopwatch.GetTimestamp();
double duration = ElapsedMs(
startMono,
endMono
);
lock (_metricsTextLock)
{
_lastScanDurationMs = duration;
if (duration > _maxScanDurationMs)
_maxScanDurationMs = duration;
}
Interlocked.Exchange(
ref _lastScanEndMono,
endMono
);
lock (_lifecycleLock)
{
if (ReferenceEquals(
_currentScanCts,
linkedCts))
{
_currentScanCts = null;
}
}
Interlocked.Exchange(ref _isScanning, 0);
_scanGate.Release();
TriggerMaintenance();
AtualizarConsole(
"Iteração finalizada, aguardando a próxima..."
);
}
}
}
private static async Task ExecutarVarreduraAsync(
long generation,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
/*
* Garante que SEN, ATU e MVD existam antes da primeira
* consulta CAN. Assim a descoberta não precisa esperar
* uma segunda varredura para encontrar os módulos.
*/
if (!Variaveis.IsAgroMonitor)
{
await AtualizarDispositivosAsync(
cancellationToken
).ConfigureAwait(false);
}
List<DispositivoDetalhesModel> mappedSnapshot =
GetDispositivosMapeadosSnapshot();
string[] ports = SerialPort.GetPortNames()
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(PortSortKey)
.ToArray();
Interlocked.Add(
ref _portsEnumerated,
ports.Length
);
HashSet<string> mappedComPorts =
new HashSet<string>(
mappedSnapshot
.Where(x =>
x != null &&
x.Tipo == TipoConexao.PortaCOM &&
!string.IsNullOrWhiteSpace(
x.Endereco
))
.Select(x => x.Endereco),
StringComparer.OrdinalIgnoreCase
);
string[] unmappedPorts = ports
.Where(x => !mappedComPorts.Contains(x))
.ToArray();
foreach (string portName in unmappedPorts)
{
cancellationToken.ThrowIfCancellationRequested();
if (generation !=
Interlocked.Read(ref _scanGeneration))
{
return;
}
await ProbePortAsync(
portName,
cancellationToken
).ConfigureAwait(false);
}
if (!Variaveis.IsAgroMonitor)
{
ProcurarDispositivosUSB();
ProcurarDispositivosEthernet();
/*
* Se o CAN já está iniciado, apenas consulta os módulos.
* Se estiver em Ethernet, Inicializar() resolve o transporte
* sem depender de uma porta COM candidata.
*/
if (CanManager.TipoServico !=
CanServiceTipo.Serial ||
CanManager.CanService.Iniciado)
{
await ProcurarDispositivosCAN(
null,
cancellationToken
).ConfigureAwait(false);
}
}
await RemoverDispositivosAusentesAsync(
ports,
cancellationToken
).ConfigureAwait(false);
await AtualizarDispositivosAsync(
cancellationToken
).ConfigureAwait(false);
}
private static async Task ProbePortAsync(
string portName,
CancellationToken cancellationToken)
{
Interlocked.Increment(ref _portsProbed);
AtualizarConsole(
"Procurando dispositivos conectados à porta " +
portName + "..."
);
SerialPort candidate = null;
bool claimed = false;
try
{
candidate = CriarPortaCandidata(portName);
if (Variaveis.IsAgroMonitor &&
!LoRaBaseService.Iniciado)
{
if (await ProcurarDispositivoLoRa(
candidate,
cancellationToken)
.ConfigureAwait(false))
{
claimed = true;
Interlocked.Increment(ref _loraFound);
return;
}
}
if (!GPSService.Iniciado)
{
if (await ProcurarDispositivoGPS(
candidate,
cancellationToken)
.ConfigureAwait(false))
{
Interlocked.Increment(ref _gpsFound);
/*
* O GPSService robusto cria sua própria instância
* de SerialPort. A candidata não é transferida.
*/
return;
}
}
if (Variaveis.IsAgroMonitor)
return;
if (CanManager.TipoServico ==
CanServiceTipo.Serial &&
!CanManager.CanService.Iniciado)
{
if (await ProcurarDispositivosCAN(
candidate,
cancellationToken)
.ConfigureAwait(false))
{
claimed = true;
Interlocked.Increment(
ref _canAdapterFound
);
AtualizarConsole(
"Dispositivo encontrado no barramento CAN"
);
return;
}
}
}
catch (UnauthorizedAccessException)
{
Interlocked.Increment(ref _portsBusy);
}
catch (OperationCanceledException)
{
throw;
}
catch (IOException ex)
{
Interlocked.Increment(ref _portsProbeErrors);
RecordError(
"[SerialService.ProbePort] " +
portName + ": " + ex.Message
);
}
catch (Exception ex)
{
Interlocked.Increment(ref _portsProbeErrors);
RecordError(
"[SerialService.ProbePort] " +
portName + ": " + ex.Message
);
}
finally
{
if (!claimed && candidate != null)
CloseAndDispose(candidate);
}
}
// ============================================================
// DETECÇÃO DE DISPOSITIVOS
// ============================================================
private static async Task<bool> ProcurarDispositivosCAN(
SerialPort porta = null,
CancellationToken cancellationToken =
default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
string name =
porta?.PortName ??
CanManager.CanService._portName ??
"CAN";
AtualizarConsole(
name +
" - Procurando dispositivos no barramento CAN"
);
/*
* Uma candidata que não é CAN é simplesmente ignorada.
* Nunca removemos mapeamentos CAN por causa desse teste.
*/
if (porta != null &&
!CanManager.CanService.PortaIsCan(porta))
{
return false;
}
if (!CanManager.CanService.Iniciado)
{
bool initialized =
CanManager.CanService.Inicializar(250);
if (!initialized)
{
AtualizarConsole(
"Adaptador " + name + " não conectado!"
);
return false;
}
}
cancellationToken.ThrowIfCancellationRequested();
if (Variaveis.OperacaoEmAndamento.DispSen != null &&
!Variaveis.OperacaoEmAndamento
.DispSen
.Dados
.Conectado)
{
AtualizarConsole(
name + " - Procurando dispositivo SEN"
);
bool found =
await Variaveis.OperacaoEmAndamento
.DispSen
.Dados
.VerificaDispositivoConectado()
.ConfigureAwait(false);
if (found)
AtualizarConsole("Dispositivo SEN encontrado");
}
cancellationToken.ThrowIfCancellationRequested();
if (Variaveis.OperacaoEmAndamento.DispAtu != null &&
!Variaveis.OperacaoEmAndamento
.DispAtu
.Dados
.Conectado)
{
AtualizarConsole(
name + " - Procurando dispositivo ATU"
);
bool found =
await Variaveis.OperacaoEmAndamento
.DispAtu
.Dados
.VerificaDispositivoConectado()
.ConfigureAwait(false);
if (found)
AtualizarConsole("Dispositivo ATU encontrado");
}
cancellationToken.ThrowIfCancellationRequested();
int expectedModules =
Variaveis.OperacaoEmAndamento
.DispMvd?
.Dados?
.Modulos?
.Count() ?? 4;
if (Variaveis.OperacaoEmAndamento.DispMvd != null &&
!MKS057DCanService.Referenciando &&
(
!MKS057DCanService.Iniciado ||
MKS057DCanService.DadosLeitura
.Count(x => x.Iniciado) <
expectedModules
))
{
AtualizarConsole(
name + " - Procurando dispositivos MKS"
);
bool found =
await MKS057DCanService
.VerificaDispositivoConectado()
.ConfigureAwait(false);
if (found)
AtualizarConsole("Dispositivo MKS encontrado");
}
cancellationToken.ThrowIfCancellationRequested();
if (Variaveis.OperacaoEmAndamento.DispMvd != null &&
(
!OIDCanService.Iniciado ||
OIDCanService.DadosLeitura
.Count(x => x.Iniciado) <
expectedModules
))
{
AtualizarConsole(
name + " - Procurando dispositivo OID"
);
bool found =
await OIDCanService
.VerificaDispositivoConectado()
.ConfigureAwait(false);
if (found)
AtualizarConsole("Dispositivo OID encontrado");
}
cancellationToken.ThrowIfCancellationRequested();
if (!DalyBMSService.Iniciado)
{
AtualizarConsole(
name + " - Procurando dispositivo BAT"
);
bool found =
await DalyBMSService
.VerificaDispositivoConectado()
.ConfigureAwait(false);
if (found)
AtualizarConsole("Dispositivo BAT encontrado");
}
return CanManager.CanService.Iniciado;
}
private static void ProcurarDispositivosUSB()
{
if (Variaveis.IsAgroMonitor)
return;
if (GeneralJoystick.JoystickConectado == null)
GeneralJoystick.AtualizaDispositivo();
var cameras = CameraWorkerService.ListaCameras;
if (cameras == null)
return;
foreach (var camera in cameras)
{
var health =
HealthWorkerService.ModulosSaude
.FirstOrDefault(
x => x.modulo == camera.dispositivo
);
if (health == null)
continue;
DispositivoDetalhesModel mapped =
GetDispositivosMapeadosSnapshot()
.FirstOrDefault(
x => x.Dispositivo ==
camera.dispositivo
);
bool connected =
health.status !=
StatusModulo.Desconectado;
if (mapped == null && connected)
{
AdicionarOuAtualizarDispositivoMapeado(
new DispositivoDetalhesModel
{
Dispositivo =
camera.dispositivo,
Endereco = "USB",
Versao = camera.versao
}
);
}
else if (mapped != null && !connected)
{
RemoverDispositivoMapeado(mapped);
}
}
}
private static async Task<bool> ProcurarDispositivoLoRa(
SerialPort porta,
CancellationToken cancellationToken)
{
if (porta == null)
return false;
cancellationToken.ThrowIfCancellationRequested();
AtualizarConsole(
porta.PortName +
" - Procurando dispositivo LRA"
);
bool found =
await LoRaBaseService
.VerificaPortaLoRa(porta)
.ConfigureAwait(false);
if (found)
{
AtualizarConsole("Dispositivo LRA encontrado");
return true;
}
ClosePortOnly(porta);
return false;
}
private static async Task<bool> ProcurarDispositivoGPS(
SerialPort porta,
CancellationToken cancellationToken)
{
if (porta == null ||
string.IsNullOrWhiteSpace(porta.PortName))
{
return false;
}
SerialPort probe =
CriarPortaCandidata(porta.PortName);
try
{
probe.ReadTimeout = 250;
probe.WriteTimeout = 500;
probe.Open();
try { probe.DiscardInBuffer(); }
catch { }
const string command =
"gngga com3 1\r\n";
byte[] commandBytes =
Encoding.ASCII.GetBytes(command);
probe.Write(
commandBytes,
0,
commandBytes.Length
);
AtualizarConsole(
probe.PortName +
" - Procurando dispositivo GPS"
);
byte[] received =
await LerAteCondicaoAsync(
probe,
maxBytes: 4096,
timeout: 1600,
predicate: IsGpsPayload,
cancellationToken:
cancellationToken
).ConfigureAwait(false);
if (!IsGpsPayload(received))
return false;
/*
* O novo GPSService copia a configuração da porta,
* cria sua própria instância e gerencia o lifecycle.
*/
GPSService.AtualizarPortaCOM(probe);
AtualizarConsole(
"Dispositivo GPS encontrado"
);
return true;
}
catch (UnauthorizedAccessException)
{
Interlocked.Increment(ref _portsBusy);
return false;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
RecordError(
"[SerialService.ProbeGPS] " +
porta.PortName + ": " + ex.Message
);
return false;
}
finally
{
CloseAndDispose(probe);
}
}
private static void ProcurarDispositivosEthernet()
{
var livox =
HealthWorkerService.ModulosSaude
.FirstOrDefault(
x => x.modulo == T_Code.Lvx
);
if (livox == null)
return;
DispositivoDetalhesModel mapped =
GetDispositivosMapeadosSnapshot()
.FirstOrDefault(
x => x.Dispositivo == livox.modulo
);
bool connected =
livox.status !=
StatusModulo.Desconectado;
if (mapped == null && connected)
{
AdicionarOuAtualizarDispositivoMapeado(
new DispositivoDetalhesModel
{
Dispositivo = livox.modulo,
Endereco =
LivoxManagerProcess
.DadosLeitura
.lidar_ip,
Versao =
LivoxManagerProcess
.DadosLeitura
.firmware_version,
Mod_ID =
LivoxManagerProcess
.DadosLeitura
.dev_type
}
);
}
else if (mapped != null && !connected)
{
RemoverDispositivoMapeado(mapped);
}
}
// ============================================================
// REMOÇÃO E ATUALIZAÇÃO DE MODELOS
// ============================================================
private static async Task RemoverDispositivosAusentesAsync(
string[] currentPorts,
CancellationToken cancellationToken)
{
var current =
new HashSet<string>(
currentPorts ?? new string[0],
StringComparer.OrdinalIgnoreCase
);
List<DispositivoDetalhesModel> mappedCom =
GetDispositivosMapeadosSnapshot()
.Where(x =>
x != null &&
x.Tipo == TipoConexao.PortaCOM &&
!string.IsNullOrWhiteSpace(x.Endereco))
.ToList();
foreach (DispositivoDetalhesModel present in mappedCom)
{
if (current.Contains(present.Endereco))
{
int ignored;
_missingPortConfirmations.TryRemove(
BuildMissingPortKey(present),
out ignored
);
}
}
foreach (DispositivoDetalhesModel device in mappedCom)
{
cancellationToken.ThrowIfCancellationRequested();
if (current.Contains(device.Endereco))
continue;
string missingKey =
BuildMissingPortKey(device);
int confirmations =
_missingPortConfirmations.AddOrUpdate(
missingKey,
1,
(_, previous) => previous + 1
);
if (confirmations <
Math.Max(1, ConfirmacoesPortaAusente))
{
continue;
}
int ignored;
_missingPortConfirmations.TryRemove(
missingKey,
out ignored
);
if (!RemoverDispositivoMapeado(device))
continue;
Interlocked.Increment(ref _devicesRemoved);
switch (device.Dispositivo)
{
case T_Code.Gps:
try
{
await GPSService
.EncerrarAsync()
.ConfigureAwait(false);
}
catch (Exception ex)
{
RecordError(
"[SerialService.RemoveGPS] " +
ex.Message
);
}
break;
case T_Code.Lra:
try
{
CloseAndDispose(
LoRaBaseService._PortaLoRa
);
}
catch { }
LoRaBaseService._PortaLoRa = null;
break;
}
}
}
public static void AtualizarDispositivos()
{
AtualizarDispositivosAsync(
CancellationToken.None
).GetAwaiter().GetResult();
}
private static async Task AtualizarDispositivosAsync(
CancellationToken cancellationToken)
{
await _updateDevicesGate
.WaitAsync(cancellationToken)
.ConfigureAwait(false);
try
{
bool firstExecution;
lock (_connectedDevicesLock)
{
firstExecution =
!Variaveis.DispositivosConectados.Any();
foreach (T_Code code in
DispositivosConexaoInicial)
{
IDispositivosService service =
Variaveis.DispositivosConectados
.FirstOrDefault(
x => x.Dispositivo == code
);
if (service != null)
continue;
List<string> moduleIds =
GetDispositivosMapeadosSnapshot()
.Where(
x => x.Dispositivo == code
)
.Select(x => x.Mod_ID)
.Where(
x => !string.IsNullOrWhiteSpace(x)
)
.ToList();
service =
DispositivosServiceFactory
.CreateDispositivoService(
code,
"Nome: " +
Enum.GetName(
typeof(T_Code),
code
),
"Descrição: " +
string.Join(
", ",
moduleIds.ToArray()
)
);
service.CarregarParametrosModulos();
Variaveis.DispositivosConectados
.Add(service);
service.Dados.IniciarRegistrador();
}
}
if (firstExecution)
DefinirModoInicialNaUi();
}
finally
{
_updateDevicesGate.Release();
}
}
private static void DefinirModoInicialNaUi()
{
if (frmInstancial.frmPrincipal == null)
return;
ComboBox combo =
frmInstancial.frmPrincipal
.cmbModoOperacao;
if (combo == null ||
combo.IsDisposed ||
combo.Disposing)
{
return;
}
Action action = () =>
{
if (!combo.IsDisposed &&
!combo.Disposing)
{
combo.SelectedIndex =
(int)ModoOperacao.MapaGPS;
}
};
try
{
if (combo.InvokeRequired)
combo.BeginInvoke(action);
else
action();
}
catch
{
// A interface pode estar fechando.
}
}
// ============================================================
// LEITURA SERIAL COM TIMEOUT
// ============================================================
public static string ReadLineWithTimeout(
SerialPort porta,
int timeout)
{
if (porta == null)
return string.Empty;
SemaphoreSlim gate = GetPortIoLock(porta);
bool acquired = false;
try
{
acquired = gate.Wait(
Math.Max(1, timeout)
);
if (!acquired || !porta.IsOpen)
return string.Empty;
int oldTimeout = porta.ReadTimeout;
try
{
porta.ReadTimeout =
Math.Max(1, timeout);
return porta.ReadLine();
}
catch (TimeoutException)
{
return string.Empty;
}
finally
{
try { porta.ReadTimeout = oldTimeout; }
catch { }
}
}
catch (IOException ex)
{
RecordError(
"[SerialService.ReadLine] " +
SafePortName(porta) + ": " + ex.Message
);
return string.Empty;
}
catch (Exception ex)
{
RecordError(
"[SerialService.ReadLine] " +
SafePortName(porta) + ": " + ex.Message
);
return string.Empty;
}
finally
{
if (acquired)
gate.Release();
}
}
public static byte[] LerDadosDaPortaSerial(
SerialPort porta,
int tamanhoEsperado,
int Timeout = 500)
{
return LerDadosDaPortaSerialAsync(
porta,
tamanhoEsperado,
Timeout,
CancellationToken.None
).GetAwaiter().GetResult();
}
public static Task<byte[]> LerDadosDaPortaSerialAsync(
SerialPort porta,
int tamanhoEsperado,
int Timeout = 500)
{
return LerDadosDaPortaSerialAsync(
porta,
tamanhoEsperado,
Timeout,
CancellationToken.None
);
}
public static async Task<byte[]> LerDadosDaPortaSerialAsync(
SerialPort porta,
int tamanhoEsperado,
int Timeout,
CancellationToken cancellationToken)
{
if (porta == null ||
tamanhoEsperado <= 0 ||
Timeout <= 0)
{
return new byte[0];
}
return await LerAteCondicaoAsync(
porta,
maxBytes: Math.Max(
tamanhoEsperado,
1
),
timeout: Timeout,
predicate: bytes =>
bytes != null &&
bytes.Length >= tamanhoEsperado,
cancellationToken:
cancellationToken
).ConfigureAwait(false);
}
private static async Task<byte[]> LerAteCondicaoAsync(
SerialPort porta,
int maxBytes,
int timeout,
Func<byte[], bool> predicate,
CancellationToken cancellationToken)
{
if (porta == null ||
maxBytes <= 0 ||
timeout <= 0)
{
return new byte[0];
}
SemaphoreSlim gate = GetPortIoLock(porta);
var buffer = new List<byte>(
Math.Min(maxBytes, 4096)
);
using (var timeoutCts =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken))
{
timeoutCts.CancelAfter(timeout);
bool acquired = false;
try
{
await gate.WaitAsync(timeoutCts.Token)
.ConfigureAwait(false);
acquired = true;
if (!porta.IsOpen)
return buffer.ToArray();
while (!timeoutCts.IsCancellationRequested &&
buffer.Count < maxBytes)
{
int available;
try
{
available = porta.BytesToRead;
}
catch
{
break;
}
if (available <= 0)
{
await Task.Delay(
5,
timeoutCts.Token
).ConfigureAwait(false);
continue;
}
int readSize = Math.Min(
available,
maxBytes - buffer.Count
);
byte[] temp = new byte[readSize];
int read = porta.Read(
temp,
0,
temp.Length
);
if (read > 0)
buffer.AddRange(temp.Take(read));
byte[] current = buffer.ToArray();
if (predicate != null &&
predicate(current))
{
return current;
}
}
}
catch (OperationCanceledException)
{
// Retorna os bytes parciais já recebidos.
}
catch (Exception ex)
{
RecordError(
"[SerialService.ReadBytes] " +
SafePortName(porta) + ": " + ex.Message
);
}
finally
{
if (acquired)
gate.Release();
}
}
return buffer.ToArray();
}
// ============================================================
// MANUTENÇÃO SEM SOBREPOSIÇÃO
// ============================================================
private static void TriggerMaintenance()
{
TriggerVersioningIfDue();
TriggerSyncIfDue();
}
private static void TriggerVersioningIfDue()
{
long now = Stopwatch.GetTimestamp();
if (!IsDue(
Interlocked.Read(
ref _lastVersioningStartMono
),
IntervaloVersionamentoMs,
now))
{
return;
}
if (!_versioningGate.Wait(0))
return;
Interlocked.Exchange(
ref _lastVersioningStartMono,
now
);
_ = RunVersioningAsync();
}
private static async Task RunVersioningAsync()
{
Interlocked.Increment(ref _versioningRuns);
try
{
await FuncoesGlobais.SafeExecuteAsync(
async () =>
{
await VersionamentoService
.AtualizarArquivoVersionamento(
false
);
}
).ConfigureAwait(false);
}
catch (Exception ex)
{
Interlocked.Increment(
ref _versioningErrors
);
RecordError(
"[SerialService.Versioning] " +
ex.Message
);
}
finally
{
_versioningGate.Release();
}
}
private static void TriggerSyncIfDue()
{
long now = Stopwatch.GetTimestamp();
if (!IsDue(
Interlocked.Read(
ref _lastSyncStartMono
),
IntervaloSincronizacaoMs,
now))
{
return;
}
if (!_syncDataGate.Wait(0))
return;
Interlocked.Exchange(
ref _lastSyncStartMono,
now
);
_ = RunSyncAsync();
}
private static async Task RunSyncAsync()
{
Interlocked.Increment(ref _syncRuns);
try
{
await FuncoesGlobais.SafeExecuteAsync(
async () =>
{
await SyncDataService
.SincronizarArquivosComServidor();
}
).ConfigureAwait(false);
}
catch (Exception ex)
{
Interlocked.Increment(ref _syncErrors);
RecordError(
"[SerialService.Sync] " + ex.Message
);
}
finally
{
_syncDataGate.Release();
}
}
// ============================================================
// LISTA DE DISPOSITIVOS MAPEADOS
// ============================================================
public static List<DispositivoDetalhesModel>
GetDispositivosMapeadosSnapshot()
{
lock (_mappedDevicesLock)
{
return DispositivosMapeados
.Where(x => x != null)
.ToList();
}
}
public static void AdicionarOuAtualizarDispositivoMapeado(
DispositivoDetalhesModel device)
{
if (device == null)
return;
lock (_mappedDevicesLock)
{
DispositivoDetalhesModel existing =
DispositivosMapeados.FirstOrDefault(
x =>
x != null &&
x.Dispositivo == device.Dispositivo &&
string.Equals(
x.Endereco ?? string.Empty,
device.Endereco ?? string.Empty,
StringComparison.OrdinalIgnoreCase
) &&
string.Equals(
x.Mod_ID ?? string.Empty,
device.Mod_ID ?? string.Empty,
StringComparison.OrdinalIgnoreCase
)
);
if (existing == null)
{
DispositivosMapeados.Add(device);
return;
}
existing.Versao = device.Versao;
}
}
public static bool RemoverDispositivoMapeado(
DispositivoDetalhesModel device)
{
if (device == null)
return false;
lock (_mappedDevicesLock)
return DispositivosMapeados.Remove(device);
}
// ============================================================
// UI E DIAGNÓSTICO
// ============================================================
private static void AtualizarConsole(string message)
{
Variaveis.MostrarLog(message);
if (Variaveis.IsAgroMonitor)
return;
_pendingUiMessage = message;
if (Interlocked.Exchange(
ref _uiUpdateScheduled,
1) == 1)
{
return;
}
Form principal = frmInstancial.frmPrincipal;
if (principal == null ||
principal.IsDisposed ||
principal.Disposing ||
!principal.IsHandleCreated)
{
Interlocked.Exchange(
ref _uiUpdateScheduled,
0
);
return;
}
try
{
principal.BeginInvoke(
(MethodInvoker)(() =>
{
try
{
string pending =
_pendingUiMessage;
if (frmInstancial.frmPrincipal != null &&
!frmInstancial.frmPrincipal.IsDisposed)
{
frmInstancial.frmPrincipal
.lblStatusConexao
.Text = pending;
}
if (frmInstancial.frmIHM != null &&
frmInstancial.frmIHM.IsHandleCreated &&
!frmInstancial.frmIHM.IsDisposed)
{
frmInstancial.frmIHM
.lblStatus
.Text = pending;
}
}
finally
{
Interlocked.Exchange(
ref _uiUpdateScheduled,
0
);
/*
* Se outra mensagem chegou durante o callback,
* agenda somente a mais recente.
*/
if (!string.Equals(
_pendingUiMessage,
message,
StringComparison.Ordinal))
{
AtualizarConsole(
_pendingUiMessage
);
}
}
})
);
}
catch
{
Interlocked.Exchange(
ref _uiUpdateScheduled,
0
);
}
}
public static string MostrarComandoResposta(
T_Code tipo,
byte[] comando,
byte[] resposta)
{
string msg =
tipo +
" - TX: " +
string.Join(
" ",
(comando ?? new byte[0])
.Select(x => x.ToString("X2"))
) +
", RX: " +
string.Join(
" ",
(resposta ?? new byte[0])
.Select(x => x.ToString("X2"))
);
Variaveis.MostrarLog(msg);
return msg;
}
// ============================================================
// SHUTDOWN
// ============================================================
public static async Task EncerrarAsync()
{
if (Interlocked.Exchange(
ref _isClosing,
1) == 1)
{
return;
}
CancellationTokenSource lifetime;
CancellationTokenSource scan;
lock (_lifecycleLock)
{
lifetime = _lifetimeCts;
scan = _currentScanCts;
}
try { scan?.Cancel(); }
catch { }
try { lifetime?.Cancel(); }
catch { }
/*
* Aguarda a varredura liberar o gate.
*/
await _scanGate.WaitAsync()
.ConfigureAwait(false);
_scanGate.Release();
lock (_lifecycleLock)
{
if (ReferenceEquals(
_lifetimeCts,
lifetime))
{
_lifetimeCts = null;
}
_currentScanCts = null;
}
if (lifetime != null)
lifetime.Dispose();
Interlocked.Exchange(ref _isScanning, 0);
}
// ============================================================
// MÉTRICAS
// ============================================================
public static SerialServiceMetrics GetMetrics()
{
string error;
double lastDuration;
double maxDuration;
lock (_metricsTextLock)
{
error = _lastError;
lastDuration = _lastScanDurationMs;
maxDuration = _maxScanDurationMs;
}
return new SerialServiceMetrics
{
IsScanning =
Volatile.Read(ref _isScanning) == 1,
IsClosing =
Volatile.Read(ref _isClosing) == 1,
ScanGeneration =
Interlocked.Read(ref _scanGeneration),
ScanStarted =
Interlocked.Read(ref _scanStarted),
ScanCompleted =
Interlocked.Read(ref _scanCompleted),
ScanSkipped =
Interlocked.Read(ref _scanSkipped),
ScanCanceled =
Interlocked.Read(ref _scanCanceled),
ScanErrors =
Interlocked.Read(ref _scanErrors),
LastScanStartAgeMs =
AgeMs(
Interlocked.Read(
ref _lastScanStartMono
)
),
LastScanEndAgeMs =
AgeMs(
Interlocked.Read(
ref _lastScanEndMono
)
),
LastScanDurationMs = lastDuration,
MaxScanDurationMs = maxDuration,
PortsEnumerated =
Interlocked.Read(
ref _portsEnumerated
),
PortsProbed =
Interlocked.Read(ref _portsProbed),
PortsBusy =
Interlocked.Read(ref _portsBusy),
PortsProbeErrors =
Interlocked.Read(
ref _portsProbeErrors
),
GpsFound =
Interlocked.Read(ref _gpsFound),
LoraFound =
Interlocked.Read(ref _loraFound),
CanAdapterFound =
Interlocked.Read(
ref _canAdapterFound
),
DevicesRemoved =
Interlocked.Read(
ref _devicesRemoved
),
SerialWriteAttempts =
Interlocked.Read(
ref _serialWriteAttempts
),
SerialWriteSuccesses =
Interlocked.Read(
ref _serialWriteSuccesses
),
SerialWriteTimeouts =
Interlocked.Read(
ref _serialWriteTimeouts
),
SerialWriteErrors =
Interlocked.Read(
ref _serialWriteErrors
),
VersioningRuns =
Interlocked.Read(
ref _versioningRuns
),
VersioningErrors =
Interlocked.Read(
ref _versioningErrors
),
SyncRuns =
Interlocked.Read(ref _syncRuns),
SyncErrors =
Interlocked.Read(ref _syncErrors),
MappedDevices =
GetDispositivosMapeadosSnapshot()
.Count,
LastError = error
};
}
// ============================================================
// HELPERS
// ============================================================
private static List<DispositivoDetalhesModel>
CriarListaInicialDispositivos()
{
string ip = string.Empty;
try
{
ip = EthernetService.ObterIpAtual(
VariaveisEquipamento
.Parametros
.comunicacao_interface
);
}
catch
{
ip = string.Empty;
}
return new List<DispositivoDetalhesModel>
{
new DispositivoDetalhesModel
{
Dispositivo = T_Code.Npc,
Endereco = ip,
Versao = Variaveis.Versao,
Mod_ID = string.Empty
}
};
}
private static SerialPort CriarPortaCandidata(
string portName)
{
return new SerialPort
{
PortName = portName,
BaudRate = 115200,
ReadTimeout = 500,
WriteTimeout = 500,
Handshake = Handshake.None,
DtrEnable = false,
RtsEnable = false,
Encoding = Encoding.ASCII
};
}
private static SemaphoreSlim GetPortIoLock(
SerialPort port)
{
string key = SafePortName(port);
if (string.IsNullOrWhiteSpace(key))
{
key =
"instance:" +
RuntimeHelpersCompat.GetIdentityHashCode(
port
);
}
return _portIoLocks.GetOrAdd(
key,
_ => new SemaphoreSlim(1, 1)
);
}
private static bool ValidarFaixaBuffer(
byte[] buffer,
int offset,
int count)
{
return buffer != null &&
offset >= 0 &&
count >= 0 &&
offset <= buffer.Length &&
count <= buffer.Length - offset;
}
private static bool IsGpsPayload(byte[] bytes)
{
if (bytes == null || bytes.Length == 0)
return false;
string text =
Encoding.ASCII.GetString(bytes);
string[] markers =
{
"$GPTXT",
"$GPRMC",
"$GNRMC",
"$GPGGA",
"$GNGGA",
"$GLGGA",
"$GPGLL",
"$GPVTG",
"$GNVTG",
"$GPGSV",
"$GLGSV",
"$GNTHS",
"$GPTHS",
"$command,"
};
return markers.Any(
x => text.IndexOf(
x,
StringComparison.OrdinalIgnoreCase
) >= 0
);
}
private static void EnsureLifetimeAvailable()
{
lock (_lifecycleLock)
{
if (_lifetimeCts == null ||
_lifetimeCts
.IsCancellationRequested)
{
_lifetimeCts?.Dispose();
_lifetimeCts =
new CancellationTokenSource();
Interlocked.Exchange(
ref _isClosing,
0
);
}
}
}
private static bool IsDue(
long previousTimestamp,
int intervalMs,
long now)
{
return previousTimestamp <= 0 ||
ElapsedMs(
previousTimestamp,
now
) >= Math.Max(1000, intervalMs);
}
private static double AgeMs(long timestamp)
{
if (timestamp <= 0)
return double.PositiveInfinity;
return ElapsedMs(
timestamp,
Stopwatch.GetTimestamp()
);
}
private static double ElapsedMs(
long start,
long end)
{
if (start <= 0 || end <= start)
return 0;
return (end - start) *
1000.0 /
Stopwatch.Frequency;
}
private static string BuildMissingPortKey(
DispositivoDetalhesModel device)
{
if (device == null)
return string.Empty;
return device.Dispositivo + "|" +
(device.Endereco ?? string.Empty) + "|" +
(device.Mod_ID ?? string.Empty);
}
private static string SafePortName(
SerialPort port)
{
if (port == null)
return string.Empty;
try { return port.PortName ?? string.Empty; }
catch { return string.Empty; }
}
private static int PortSortKey(string port)
{
if (string.IsNullOrWhiteSpace(port))
return int.MaxValue;
string digits =
new string(
port.Where(char.IsDigit).ToArray()
);
int number;
return int.TryParse(digits, out number)
? number
: int.MaxValue - 1;
}
private static void ClosePortOnly(
SerialPort port)
{
if (port == null)
return;
try
{
if (port.IsOpen)
port.Close();
}
catch { }
}
private static void CloseAndDispose(
SerialPort port)
{
if (port == null)
return;
ClosePortOnly(port);
try { port.Dispose(); }
catch { }
}
private static void RecordError(string error)
{
lock (_metricsTextLock)
_lastError = error;
Variaveis.MostrarLog(error);
}
/// <summary>
/// Evita depender de RuntimeHelpers em targets antigos.
/// </summary>
private static class RuntimeHelpersCompat
{
public static int GetIdentityHashCode(object value)
{
return value == null
? 0
: System.Runtime.CompilerServices
.RuntimeHelpers
.GetHashCode(value);
}
}
}
public sealed class SerialServiceMetrics
{
public bool IsScanning { get; set; }
public bool IsClosing { get; set; }
public long ScanGeneration { get; set; }
public long ScanStarted { get; set; }
public long ScanCompleted { get; set; }
public long ScanSkipped { get; set; }
public long ScanCanceled { get; set; }
public long ScanErrors { get; set; }
public double LastScanStartAgeMs { get; set; }
public double LastScanEndAgeMs { get; set; }
public double LastScanDurationMs { get; set; }
public double MaxScanDurationMs { get; set; }
public long PortsEnumerated { get; set; }
public long PortsProbed { get; set; }
public long PortsBusy { get; set; }
public long PortsProbeErrors { get; set; }
public long GpsFound { get; set; }
public long LoraFound { get; set; }
public long CanAdapterFound { get; set; }
public long DevicesRemoved { get; set; }
public long SerialWriteAttempts { get; set; }
public long SerialWriteSuccesses { get; set; }
public long SerialWriteTimeouts { get; set; }
public long SerialWriteErrors { get; set; }
public long VersioningRuns { get; set; }
public long VersioningErrors { get; set; }
public long SyncRuns { get; set; }
public long SyncErrors { get; set; }
public int MappedDevices { get; set; }
public string LastError { get; set; }
}
}