2472 lines
74 KiB
C#
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; }
|
|
}
|
|
}
|