3548 lines
110 KiB
C#
3548 lines
110 KiB
C#
using AgroBase.Models;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.IO.Ports;
|
|
using System.Linq;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using static AgroBase.Models.Enums;
|
|
|
|
namespace AgroBase.Services
|
|
{
|
|
/// <summary>
|
|
/// Serviço GNSS/RTCM do rover.
|
|
///
|
|
/// Princípios:
|
|
/// - leitura serial nunca executa rede, disco ou tarefas lentas;
|
|
/// - toda escrita no UM982 passa por um único lock;
|
|
/// - RTCM entra em fila limitada e mensagens velhas são descartadas;
|
|
/// - NTRIP e MQTT compartilham o mesmo trilho de escrita;
|
|
/// - intervalos e idades usam relógio monotônico;
|
|
/// - configuração do módulo é exclusiva;
|
|
/// - mantém as assinaturas públicas usadas pelo projeto legado.
|
|
/// </summary>
|
|
public class GPSService
|
|
{
|
|
// ============================================================
|
|
// CONTRATO PÚBLICO LEGADO
|
|
// ============================================================
|
|
|
|
public static SerialPort PortaGPS = null;
|
|
|
|
/// <summary>
|
|
/// Estado puro. Consultar esta propriedade não abre nem altera a porta.
|
|
/// </summary>
|
|
public static bool Iniciado
|
|
{
|
|
get
|
|
{
|
|
SerialPort porta = PortaGPS;
|
|
return porta != null && porta.IsOpen;
|
|
}
|
|
}
|
|
|
|
public static GPSModel UltimaLeitura = new GPSModel();
|
|
public static GPSModel PenultimaLeitura = new GPSModel();
|
|
public static List<GPSModel> UltimasLeituras = new List<GPSModel>();
|
|
public static List<string> Logs = new List<string>();
|
|
|
|
public static int TaxaAmostragemHz { get; set; } = 5;
|
|
|
|
private static bool InverterHeading = true;
|
|
private static int rtk_timeout = 60;
|
|
private static int TempoMin_Ntrip = 10;
|
|
|
|
private static volatile bool LoopRTK_Ntrip = false;
|
|
public static volatile bool CorrecaoRTK_Ntrip = false;
|
|
|
|
/// <summary>
|
|
/// Mantido para compatibilidade e diagnóstico humano.
|
|
/// Watchdogs internos usam Stopwatch.
|
|
/// </summary>
|
|
public static DateTime UltimoEnvioCorrecaoRTK = DateTime.MinValue;
|
|
|
|
public static GeoLeverArm LeverArm = new GeoLeverArm(
|
|
offsetFisicoFrontalCm: VariaveisEquipamento.LeverArmFrontalCm,
|
|
offsetFisicoLateralCm: VariaveisEquipamento.LeverArmLateralCm,
|
|
mirrL: true,
|
|
invF: true,
|
|
invH: false
|
|
);
|
|
|
|
public static Queue<GPSModel> historicoPosicao = new Queue<GPSModel>();
|
|
|
|
// ============================================================
|
|
// SINCRONIZAÇÃO E CICLO DE VIDA
|
|
// ============================================================
|
|
|
|
private static readonly object _stateLock = new object();
|
|
private static readonly object _bufferLock = new object();
|
|
private static readonly object _rtcmQueueLock = new object();
|
|
private static readonly object _workerLock = new object();
|
|
private static readonly object _portLifecycleLock = new object();
|
|
|
|
private static readonly SemaphoreSlim _serialWriteLock = new SemaphoreSlim(1, 1);
|
|
private static readonly SemaphoreSlim _configurationLock = new SemaphoreSlim(1, 1);
|
|
private static readonly SemaphoreSlim _rtcmSignal = new SemaphoreSlim(0, int.MaxValue);
|
|
private static readonly SemaphoreSlim _logSignal = new SemaphoreSlim(0, int.MaxValue);
|
|
private static readonly SemaphoreSlim _localPublishSignal = new SemaphoreSlim(0, int.MaxValue);
|
|
|
|
private static CancellationTokenSource _serviceCts;
|
|
private static Task _nmeaWorkerTask;
|
|
private static Task _rtcmWriterTask;
|
|
private static Task _logWriterTask;
|
|
private static Task _localPublisherTask;
|
|
|
|
private static CancellationTokenSource _configurationCts;
|
|
|
|
private static CancellationTokenSource _ntripCts;
|
|
private static Task _ntripTask;
|
|
private static TcpClient _ntripClient;
|
|
|
|
private static long _portGeneration;
|
|
|
|
// ============================================================
|
|
// BUFFERS E FILAS
|
|
// ============================================================
|
|
|
|
private static readonly StringBuilder _nmeaBuffer = new StringBuilder(8192);
|
|
|
|
private const int MaxNmeaBufferChars = 64 * 1024;
|
|
|
|
private static readonly object _nmeaQueueLock = new object();
|
|
private static readonly Queue<string> _nmeaQueue = new Queue<string>();
|
|
private static readonly SemaphoreSlim _nmeaSignal = new SemaphoreSlim(0, int.MaxValue);
|
|
private const int NmeaQueueCapacity = 512;
|
|
|
|
private static readonly Queue<RtcmEnvelope> _rtcmQueue = new Queue<RtcmEnvelope>();
|
|
|
|
private const int RtcmQueueCapacity = 48;
|
|
private const int RtcmMaxAgeMs = 2500;
|
|
private const int RtcmMaxPayloadBytes = 64 * 1024;
|
|
|
|
private static long _rtcmSequence;
|
|
|
|
private static readonly ConcurrentQueue<string> _gpsLogQueue = new ConcurrentQueue<string>();
|
|
|
|
private static readonly object _localPublishLock = new object();
|
|
private static readonly Dictionary<string, string> _localLatestPayload = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
|
|
// ============================================================
|
|
// MÉTRICAS
|
|
// ============================================================
|
|
|
|
private static long _lastSerialRxMono;
|
|
private static long _lastValidNmeaMono;
|
|
private static long _lastValidGgaMono;
|
|
private static long _lastValidThsMono;
|
|
private static long _lastRtcmReceivedMono;
|
|
private static long _lastRtcmWrittenMono;
|
|
|
|
private static long _serialBytesReceived;
|
|
private static long _nmeaValid;
|
|
private static long _nmeaInvalid;
|
|
private static long _nmeaChecksumErrors;
|
|
private static long _nmeaBufferResets;
|
|
private static long _nmeaQueued;
|
|
private static long _nmeaDroppedQueue;
|
|
|
|
private static long _rtcmReceived;
|
|
private static long _rtcmQueued;
|
|
private static long _rtcmWritten;
|
|
private static long _rtcmDroppedQueue;
|
|
private static long _rtcmDroppedStale;
|
|
private static long _rtcmDroppedInvalid;
|
|
private static long _rtcmWriteErrors;
|
|
|
|
private static long _configurationRuns;
|
|
private static long _configurationErrors;
|
|
|
|
private static long _ntripReconnects;
|
|
private static long _ntripBytesReceived;
|
|
private static long _ntripErrors;
|
|
|
|
private static string _lastError;
|
|
private static readonly object _metricsTextLock = new object();
|
|
|
|
// ============================================================
|
|
// INICIALIZAÇÃO DA PORTA
|
|
// ============================================================
|
|
|
|
public static void AtualizarPortaCOM(SerialPort porta)
|
|
{
|
|
if (porta == null)
|
|
throw new ArgumentNullException(nameof(porta));
|
|
|
|
EnsureBackgroundWorkersStarted();
|
|
|
|
long generation = Interlocked.Increment(ref _portGeneration);
|
|
|
|
/*
|
|
* A troca da porta participa do mesmo trilho de escrita.
|
|
* Assim nenhum RTCM ou comando ASCII escreve enquanto a
|
|
* SerialPort está sendo fechada e substituída.
|
|
*/
|
|
_serialWriteLock.Wait();
|
|
|
|
try
|
|
{
|
|
lock (_portLifecycleLock)
|
|
{
|
|
SerialPort anterior = PortaGPS;
|
|
|
|
if (anterior != null)
|
|
{
|
|
try
|
|
{
|
|
anterior.DataReceived -= PortaGPS_DataReceived;
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
if (anterior.IsOpen)
|
|
anterior.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.AtualizarPortaCOM] Erro ao fechar porta antiga: " +
|
|
ex.Message
|
|
);
|
|
}
|
|
|
|
try
|
|
{
|
|
anterior.Dispose();
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
var novaPorta = new SerialPort
|
|
{
|
|
BaudRate = porta.BaudRate,
|
|
PortName = porta.PortName,
|
|
ReadTimeout = 2000,
|
|
WriteTimeout = 2000,
|
|
Encoding = Encoding.ASCII,
|
|
DtrEnable = false,
|
|
RtsEnable = false
|
|
};
|
|
|
|
novaPorta.DataReceived += PortaGPS_DataReceived;
|
|
|
|
try
|
|
{
|
|
if (porta.IsOpen)
|
|
porta.Close();
|
|
}
|
|
catch { }
|
|
|
|
PortaGPS = novaPorta;
|
|
|
|
try
|
|
{
|
|
novaPorta.Open();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
novaPorta.DataReceived -= PortaGPS_DataReceived;
|
|
PortaGPS = null;
|
|
RecordError(
|
|
"[GPSService.AtualizarPortaCOM] Não foi possível abrir " +
|
|
novaPorta.PortName + ": " + ex.Message
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_serialWriteLock.Release();
|
|
}
|
|
|
|
DefinirDispositivo();
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await ConfigurarModulo().ConfigureAwait(false);
|
|
|
|
if (generation != Interlocked.Read(ref _portGeneration))
|
|
return;
|
|
|
|
if (Variaveis.IsAgroBase && CorrecaoRTK_Ntrip)
|
|
await IniciarNtripAsync().ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.AtualizarPortaCOM] Falha pós-conexão: " +
|
|
ex.Message
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void DefinirDispositivo()
|
|
{
|
|
SerialPort porta = PortaGPS;
|
|
|
|
if (porta == null || !porta.IsOpen)
|
|
return;
|
|
|
|
try
|
|
{
|
|
bool jaExiste = SerialService.DispositivosMapeados.Any(
|
|
x => x.Dispositivo == T_Code.Gps &&
|
|
string.Equals(
|
|
x.Endereco,
|
|
porta.PortName,
|
|
StringComparison.OrdinalIgnoreCase
|
|
)
|
|
);
|
|
|
|
if (!jaExiste)
|
|
{
|
|
SerialService.DispositivosMapeados.Add(
|
|
new DispositivoDetalhesModel
|
|
{
|
|
Dispositivo = T_Code.Gps,
|
|
Endereco = porta.PortName,
|
|
Versao = "1"
|
|
}
|
|
);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.DefinirDispositivo] " + ex.Message
|
|
);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// CONFIGURAÇÃO DO UM982
|
|
// ============================================================
|
|
|
|
public static async Task ConfigurarModulo()
|
|
{
|
|
EnsureBackgroundWorkersStarted();
|
|
|
|
CancellationTokenSource novaCts = new CancellationTokenSource();
|
|
CancellationTokenSource anterior =
|
|
Interlocked.Exchange(ref _configurationCts, novaCts);
|
|
|
|
if (anterior != null)
|
|
{
|
|
try { anterior.Cancel(); } catch { }
|
|
anterior.Dispose();
|
|
}
|
|
|
|
await ConfigurarModuloRover(
|
|
comprimento_antena: 100,
|
|
cancellationToken: novaCts.Token
|
|
).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task ConfigurarModuloRover(string porta_usb = "com3", string porta_entrada = "com2", int comprimento_antena = 100, int tolerancia_antena = 5, CancellationToken cancellationToken = default(CancellationToken))
|
|
{
|
|
await _configurationLock.WaitAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
Interlocked.Increment(ref _configurationRuns);
|
|
|
|
try
|
|
{
|
|
Variaveis.MostrarLog(
|
|
"[GPSService.ConfigurarModuloRover] Iniciando configuração."
|
|
);
|
|
|
|
string freq = (1.0 / Math.Max(1, TaxaAmostragemHz))
|
|
.ToString("0.0", CultureInfo.InvariantCulture);
|
|
|
|
string[] comandos =
|
|
{
|
|
"config com1 115200\r\n",
|
|
"config com2 115200\r\n",
|
|
"config com3 115200\r\n",
|
|
|
|
"unlog com1\r\n",
|
|
"unlog com2\r\n",
|
|
"unlog com3\r\n",
|
|
|
|
"mode rover uav\r\n",
|
|
|
|
"config rtk timeout " + rtk_timeout + "\r\n",
|
|
"config dgps timeout 60\r\n",
|
|
|
|
"config heading fixlength\r\n",
|
|
"config heading length " +
|
|
comprimento_antena + " " +
|
|
tolerancia_antena + "\r\n",
|
|
|
|
"gngga " + porta_usb + " " + freq + "\r\n",
|
|
"gpths " + porta_usb + " " + freq + "\r\n",
|
|
"gpvtg " + porta_usb + " " + freq + "\r\n",
|
|
|
|
"saveconfig\r\n"
|
|
};
|
|
|
|
await Task.Delay(1500, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
/*
|
|
* Mantém exclusividade durante a sequência completa.
|
|
* RTCMs que envelhecerem enquanto o módulo é configurado
|
|
* serão descartados pelo worker, em vez de serem injetados
|
|
* entre comandos ASCII.
|
|
*/
|
|
await _serialWriteLock.WaitAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
foreach (string comando in comandos)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
WriteSerialUnsafe(
|
|
Encoding.ASCII.GetBytes(comando),
|
|
comando.Length
|
|
);
|
|
|
|
await Task.Delay(350, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_serialWriteLock.Release();
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Interlocked.Increment(ref _configurationErrors);
|
|
RecordError(
|
|
"[GPSService.ConfigurarModuloRover] " + ex.Message
|
|
);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_configurationLock.Release();
|
|
}
|
|
}
|
|
|
|
private static async Task ConfigurarModuloBase(string porta_usb = "com3", string porta_saida = "com2", int tempo_fixacao = 60, CancellationToken cancellationToken = default(CancellationToken))
|
|
{
|
|
await _configurationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
Interlocked.Increment(ref _configurationRuns);
|
|
|
|
try
|
|
{
|
|
string[] comandos =
|
|
{
|
|
"config com1 115200\r\n",
|
|
"config com2 115200\r\n",
|
|
"config com3 115200\r\n",
|
|
|
|
"unlog com1\r\n",
|
|
"unlog com2\r\n",
|
|
"unlog com3\r\n",
|
|
|
|
"mode base 957 time " + tempo_fixacao + " 0\r\n",
|
|
|
|
"RTCM1006 " + porta_saida + " 10\r\n",
|
|
"RTCM1033 " + porta_saida + " 30\r\n",
|
|
"RTCM1074 " + porta_saida + " 1\r\n",
|
|
"RTCM1084 " + porta_saida + " 1\r\n",
|
|
"RTCM1094 " + porta_saida + " 1\r\n",
|
|
"RTCM1124 " + porta_saida + " 1\r\n",
|
|
"RTCM1230 " + porta_saida + " 10\r\n",
|
|
|
|
"gngga " + porta_usb + " 1\r\n",
|
|
"saveconfig\r\n"
|
|
};
|
|
|
|
await _serialWriteLock.WaitAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
foreach (string comando in comandos)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
byte[] bytes = Encoding.ASCII.GetBytes(comando);
|
|
WriteSerialUnsafe(bytes, bytes.Length);
|
|
|
|
await Task.Delay(350, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_serialWriteLock.Release();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Interlocked.Increment(ref _configurationErrors);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_configurationLock.Release();
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// RECEPÇÃO SERIAL / NMEA
|
|
// ============================================================
|
|
|
|
private static void PortaGPS_DataReceived(object sender, SerialDataReceivedEventArgs e)
|
|
{
|
|
SerialPort porta = sender as SerialPort;
|
|
|
|
if (porta == null || !porta.IsOpen)
|
|
return;
|
|
|
|
try
|
|
{
|
|
string recebido = porta.ReadExisting();
|
|
|
|
if (string.IsNullOrEmpty(recebido))
|
|
return;
|
|
|
|
Interlocked.Add(
|
|
ref _serialBytesReceived,
|
|
Encoding.ASCII.GetByteCount(recebido)
|
|
);
|
|
|
|
Interlocked.Exchange(
|
|
ref _lastSerialRxMono,
|
|
Stopwatch.GetTimestamp()
|
|
);
|
|
|
|
List<string> sentencas = ExtrairSentencasCompletas(recebido);
|
|
|
|
foreach (string sentenca in sentencas)
|
|
{
|
|
EnfileirarSentencaNmea(sentenca);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.PortaGPS_DataReceived] " + ex.Message
|
|
);
|
|
}
|
|
}
|
|
|
|
private static void EnfileirarSentencaNmea(string sentenca)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(sentenca))
|
|
return;
|
|
|
|
lock (_nmeaQueueLock)
|
|
{
|
|
while (_nmeaQueue.Count >= NmeaQueueCapacity)
|
|
{
|
|
_nmeaQueue.Dequeue();
|
|
Interlocked.Increment(ref _nmeaDroppedQueue);
|
|
}
|
|
|
|
_nmeaQueue.Enqueue(sentenca);
|
|
Interlocked.Increment(ref _nmeaQueued);
|
|
}
|
|
|
|
try { _nmeaSignal.Release(); }
|
|
catch (SemaphoreFullException) { }
|
|
}
|
|
|
|
private static async Task NmeaWorkerLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await _nmeaSignal.WaitAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
|
|
bool processouAlgo = false;
|
|
|
|
while (true)
|
|
{
|
|
string sentenca = null;
|
|
|
|
lock (_nmeaQueueLock)
|
|
{
|
|
if (_nmeaQueue.Count > 0)
|
|
sentenca = _nmeaQueue.Dequeue();
|
|
}
|
|
|
|
if (sentenca == null)
|
|
break;
|
|
|
|
ProcessarSentencaSegura(sentenca);
|
|
processouAlgo = true;
|
|
}
|
|
|
|
if (processouAlgo)
|
|
EnfileirarLogSnapshot();
|
|
}
|
|
}
|
|
|
|
private static List<string> ExtrairSentencasCompletas(string recebido)
|
|
{
|
|
var resultado = new List<string>();
|
|
|
|
lock (_bufferLock)
|
|
{
|
|
_nmeaBuffer.Append(recebido);
|
|
|
|
if (_nmeaBuffer.Length > MaxNmeaBufferChars)
|
|
{
|
|
/*
|
|
* Tenta preservar a última possível sentença.
|
|
* Caso não exista '$', limpa completamente.
|
|
*/
|
|
string acumuladoExcedido = _nmeaBuffer.ToString();
|
|
int ultimoInicio = acumuladoExcedido.LastIndexOf('$');
|
|
|
|
_nmeaBuffer.Clear();
|
|
|
|
if (ultimoInicio >= 0 &&
|
|
acumuladoExcedido.Length - ultimoInicio < 2048)
|
|
{
|
|
_nmeaBuffer.Append(
|
|
acumuladoExcedido.Substring(ultimoInicio)
|
|
);
|
|
}
|
|
|
|
Interlocked.Increment(ref _nmeaBufferResets);
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
string acumulado = _nmeaBuffer.ToString();
|
|
int fim = acumulado.IndexOf('\n');
|
|
|
|
if (fim < 0)
|
|
break;
|
|
|
|
string linha = acumulado.Substring(0, fim)
|
|
.Trim('\r', '\n', ' ', '\t');
|
|
|
|
_nmeaBuffer.Remove(0, fim + 1);
|
|
|
|
if (!string.IsNullOrWhiteSpace(linha))
|
|
resultado.Add(linha);
|
|
}
|
|
}
|
|
|
|
return resultado;
|
|
}
|
|
|
|
private static void ProcessarSentencaSegura(string sentenca)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(sentenca))
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (!ValidarChecksumNmea(sentenca))
|
|
{
|
|
Interlocked.Increment(ref _nmeaChecksumErrors);
|
|
Interlocked.Increment(ref _nmeaInvalid);
|
|
return;
|
|
}
|
|
|
|
lock (_stateLock)
|
|
{
|
|
ProcessarDadosNMEA(sentenca);
|
|
}
|
|
|
|
Interlocked.Increment(ref _nmeaValid);
|
|
Interlocked.Exchange(
|
|
ref _lastValidNmeaMono,
|
|
Stopwatch.GetTimestamp()
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Interlocked.Increment(ref _nmeaInvalid);
|
|
RecordError(
|
|
"[GPSService.ProcessarSentencaSegura] Sentença '" +
|
|
sentenca + "': " + ex.Message
|
|
);
|
|
}
|
|
}
|
|
|
|
private static bool ValidarChecksumNmea(string sentenca)
|
|
{
|
|
if (sentenca.StartsWith(
|
|
"$command,",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!sentenca.StartsWith("$", StringComparison.Ordinal))
|
|
return false;
|
|
|
|
int asterisco = sentenca.LastIndexOf('*');
|
|
|
|
/*
|
|
* Alguns ACKs e firmwares podem não fornecer checksum.
|
|
* Sentenças de navegação sem '*' são aceitas por compatibilidade,
|
|
* mas as que possuem checksum são verificadas rigorosamente.
|
|
*/
|
|
if (asterisco < 0)
|
|
return true;
|
|
|
|
if (asterisco + 2 >= sentenca.Length)
|
|
return false;
|
|
|
|
byte calculado = 0;
|
|
|
|
for (int i = 1; i < asterisco; i++)
|
|
calculado ^= (byte)sentenca[i];
|
|
|
|
byte informado;
|
|
|
|
return byte.TryParse(
|
|
sentenca.Substring(asterisco + 1, 2),
|
|
NumberStyles.HexNumber,
|
|
CultureInfo.InvariantCulture,
|
|
out informado
|
|
) &&
|
|
calculado == informado;
|
|
}
|
|
|
|
private static void ProcessarDadosNMEA(string sentenca)
|
|
{
|
|
DateTime agora = DateTime.Now;
|
|
|
|
if (sentenca.StartsWith("$GPGGA"))
|
|
{
|
|
ProcessarGPGGA(sentenca);
|
|
AtualizarCoordenadasGPS();
|
|
MarkValidGga();
|
|
}
|
|
else if (sentenca.StartsWith("$GNGGA") ||
|
|
sentenca.StartsWith("$GLGGA"))
|
|
{
|
|
ProcessarGNGGA(sentenca);
|
|
AtualizarCoordenadasGPS();
|
|
MarkValidGga();
|
|
}
|
|
else if (sentenca.StartsWith("$GNRMC"))
|
|
{
|
|
ProcessarGxRMC(sentenca);
|
|
}
|
|
else if (sentenca.StartsWith("$GNVTG") ||
|
|
sentenca.StartsWith("$GPVTG"))
|
|
{
|
|
ProcessarGNVTG(sentenca);
|
|
}
|
|
else if (sentenca.StartsWith("$GPGSV") ||
|
|
sentenca.StartsWith("$GLGSV") ||
|
|
sentenca.StartsWith("$GBGSV") ||
|
|
sentenca.StartsWith("$GAGSV"))
|
|
{
|
|
ProcessarGSV(sentenca);
|
|
}
|
|
else if (sentenca.StartsWith("$GNTHS") ||
|
|
sentenca.StartsWith("$GPTHS") ||
|
|
sentenca.StartsWith("$GATHS"))
|
|
{
|
|
ProcessarGNTHS(sentenca);
|
|
|
|
Interlocked.Exchange(
|
|
ref _lastValidThsMono,
|
|
Stopwatch.GetTimestamp()
|
|
);
|
|
}
|
|
else if (sentenca.Length > 6 &&
|
|
sentenca[3] == 'G' &&
|
|
sentenca[4] == 'S' &&
|
|
sentenca[5] == 'A')
|
|
{
|
|
ProcessarGxGSA(sentenca);
|
|
}
|
|
else if (sentenca.Length > 6 &&
|
|
sentenca[3] == 'R' &&
|
|
sentenca[4] == 'M' &&
|
|
sentenca[5] == 'C')
|
|
{
|
|
ProcessarGxRMC(sentenca);
|
|
}
|
|
else if (sentenca.StartsWith(
|
|
"$command,",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Variaveis.MostrarLog(
|
|
"[GPSService.ProcessarDadosNMEA] " + sentenca
|
|
);
|
|
}
|
|
|
|
PenultimaLeitura.UltimoComandoRespondido =
|
|
UltimaLeitura.UltimoComandoRespondido;
|
|
|
|
UltimaLeitura.UltimoComandoRespondido = agora;
|
|
}
|
|
|
|
private static void MarkValidGga()
|
|
{
|
|
Interlocked.Exchange(
|
|
ref _lastValidGgaMono,
|
|
Stopwatch.GetTimestamp()
|
|
);
|
|
}
|
|
|
|
private static void ProcessarGPGGA(string sentenca)
|
|
{
|
|
string[] parts = sentenca.Split(',');
|
|
|
|
if (parts.Length < 10)
|
|
throw new FormatException("GPGGA incompleta.");
|
|
|
|
CopiarPosicaoParaPenultima();
|
|
|
|
UltimaLeitura.TimestampPos.valor =
|
|
Stopwatch.GetTimestamp() /
|
|
(double)Stopwatch.Frequency;
|
|
|
|
UltimaLeitura.Momento = DateTime.Now;
|
|
|
|
double lat;
|
|
double lon;
|
|
|
|
if (TryParseDmm(parts[2], parts[3], 2, out lat) &&
|
|
TryParseDmm(parts[4], parts[5], 3, out lon))
|
|
{
|
|
UltimaLeitura.LatitudeAnt = lat;
|
|
UltimaLeitura.LongitudeAnt = lon;
|
|
AplicarPosicaoCorrigida();
|
|
}
|
|
|
|
int numSat;
|
|
double hdop;
|
|
double alt;
|
|
|
|
UltimaLeitura.NumeroSatelites =
|
|
int.TryParse(parts[7], out numSat) ? numSat : 0;
|
|
|
|
UltimaLeitura.PrecisaoHorizontal =
|
|
double.TryParse(
|
|
parts[8],
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out hdop
|
|
) ? hdop : 0;
|
|
|
|
UltimaLeitura.Altitude =
|
|
double.TryParse(
|
|
parts[9],
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out alt
|
|
) ? alt : 0;
|
|
}
|
|
|
|
private static void ProcessarGNGGA(string sentenca)
|
|
{
|
|
string[] campos = sentenca.Split(',');
|
|
|
|
if (campos.Length < 10)
|
|
throw new FormatException("GNGGA incompleta.");
|
|
|
|
string horaUtc = GetField(campos, 1);
|
|
string latRaw = GetField(campos, 2);
|
|
string latHem = GetField(campos, 3);
|
|
string lonRaw = GetField(campos, 4);
|
|
string lonHem = GetField(campos, 5);
|
|
string qualidadeRaw = GetField(campos, 6, "0");
|
|
string satRaw = GetField(campos, 7, "0");
|
|
string hdopRaw = GetField(campos, 8, "99.9");
|
|
string altRaw = GetField(campos, 9, "0");
|
|
string geoidRaw = GetField(campos, 11, "0");
|
|
string idadeRaw = GetField(campos, 13);
|
|
string baseId = StripChecksum(GetField(campos, 14));
|
|
|
|
double latitude = 0;
|
|
double longitude = 0;
|
|
bool possuiLat = TryParseDmm(
|
|
latRaw,
|
|
latHem,
|
|
2,
|
|
out latitude
|
|
);
|
|
bool possuiLon = TryParseDmm(
|
|
lonRaw,
|
|
lonHem,
|
|
3,
|
|
out longitude
|
|
);
|
|
|
|
double altMsl;
|
|
double geoid;
|
|
double hdop;
|
|
int sats;
|
|
int fixCode;
|
|
|
|
double.TryParse(
|
|
altRaw,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out altMsl
|
|
);
|
|
|
|
double.TryParse(
|
|
geoidRaw,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out geoid
|
|
);
|
|
|
|
double.TryParse(
|
|
hdopRaw,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out hdop
|
|
);
|
|
|
|
int.TryParse(satRaw, out sats);
|
|
int.TryParse(qualidadeRaw, out fixCode);
|
|
|
|
double idadeCorrecao = -1;
|
|
double idadeValida;
|
|
|
|
if (!string.IsNullOrWhiteSpace(idadeRaw) &&
|
|
double.TryParse(
|
|
idadeRaw,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out idadeValida))
|
|
{
|
|
idadeCorrecao = idadeValida;
|
|
}
|
|
|
|
CopiarPosicaoParaPenultima();
|
|
|
|
UltimaLeitura.TimestampPos.valor =
|
|
Stopwatch.GetTimestamp() /
|
|
(double)Stopwatch.Frequency;
|
|
|
|
UltimaLeitura.Momento = DateTime.Now;
|
|
|
|
if (possuiLat && possuiLon)
|
|
{
|
|
UltimaLeitura.LatitudeAnt = latitude;
|
|
UltimaLeitura.LongitudeAnt = longitude;
|
|
}
|
|
|
|
UltimaLeitura.Altitude = altMsl;
|
|
UltimaLeitura.AltitudeElipsoidal = altMsl + geoid;
|
|
UltimaLeitura.PrecisaoHorizontal = hdop;
|
|
UltimaLeitura.NumeroSatelites = sats;
|
|
UltimaLeitura.QualidadeFix = (TiposCorrecaoGPS)fixCode;
|
|
UltimaLeitura.IdadeCorrecao = idadeCorrecao;
|
|
UltimaLeitura.BaseID = baseId;
|
|
|
|
if (possuiLat && possuiLon)
|
|
AplicarPosicaoCorrigida();
|
|
|
|
DateTime dataHora;
|
|
|
|
if (TryParseNmeaTime(horaUtc, out dataHora))
|
|
UltimaLeitura.DataHora = dataHora;
|
|
|
|
if (!UltimaLeitura.EnuOriginSet &&
|
|
UltimaLeitura.QualidadeFix ==
|
|
TiposCorrecaoGPS.RTKFixo &&
|
|
possuiLat &&
|
|
possuiLon)
|
|
{
|
|
UltimaLeitura.Lat0 = latitude;
|
|
UltimaLeitura.Lon0 = longitude;
|
|
UltimaLeitura.EnuOriginSet = true;
|
|
}
|
|
}
|
|
|
|
private static void CopiarPosicaoParaPenultima()
|
|
{
|
|
PenultimaLeitura.TimestampPos =
|
|
UltimaLeitura.TimestampPos.Clone();
|
|
|
|
PenultimaLeitura.Momento = UltimaLeitura.Momento;
|
|
PenultimaLeitura.Latitude = UltimaLeitura.Latitude;
|
|
PenultimaLeitura.Longitude = UltimaLeitura.Longitude;
|
|
PenultimaLeitura.LatitudeAnt = UltimaLeitura.LatitudeAnt;
|
|
PenultimaLeitura.LongitudeAnt = UltimaLeitura.LongitudeAnt;
|
|
PenultimaLeitura.Altitude = UltimaLeitura.Altitude;
|
|
PenultimaLeitura.AltitudeElipsoidal =
|
|
UltimaLeitura.AltitudeElipsoidal;
|
|
|
|
PenultimaLeitura.PrecisaoHorizontal =
|
|
UltimaLeitura.PrecisaoHorizontal;
|
|
|
|
PenultimaLeitura.NumeroSatelites =
|
|
UltimaLeitura.NumeroSatelites;
|
|
|
|
PenultimaLeitura.QualidadeFix =
|
|
UltimaLeitura.QualidadeFix;
|
|
|
|
PenultimaLeitura.DataHora = UltimaLeitura.DataHora;
|
|
PenultimaLeitura.IdadeCorrecao =
|
|
UltimaLeitura.IdadeCorrecao;
|
|
|
|
PenultimaLeitura.BaseID = UltimaLeitura.BaseID;
|
|
}
|
|
|
|
private static void ProcessarGNVTG(string sentenca)
|
|
{
|
|
string[] campos = sentenca.Split(',');
|
|
|
|
if (campos.Length < 8)
|
|
throw new FormatException("VTG incompleta.");
|
|
|
|
double curso;
|
|
double velocidadeKmh;
|
|
|
|
double.TryParse(
|
|
campos[1],
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out curso
|
|
);
|
|
|
|
double.TryParse(
|
|
campos[7],
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out velocidadeKmh
|
|
);
|
|
|
|
PenultimaLeitura.Momento = UltimaLeitura.Momento;
|
|
PenultimaLeitura.CursoVerdadeiro =
|
|
UltimaLeitura.CursoVerdadeiro;
|
|
|
|
PenultimaLeitura.Velocidade =
|
|
UltimaLeitura.Velocidade;
|
|
|
|
UltimaLeitura.Momento = DateTime.Now;
|
|
UltimaLeitura.CursoVerdadeiro = curso;
|
|
UltimaLeitura.Velocidade = velocidadeKmh;
|
|
}
|
|
|
|
private static void ProcessarGSV(string sentenca)
|
|
{
|
|
string[] campos = sentenca.Split(',');
|
|
|
|
if (campos.Length < 4 || sentenca.Length < 3)
|
|
throw new FormatException("GSV incompleta.");
|
|
|
|
string tipoSistema = sentenca.Substring(1, 2);
|
|
|
|
int sentAtual;
|
|
int sentTotal;
|
|
int visiveis;
|
|
|
|
int.TryParse(campos[2], out sentAtual);
|
|
int.TryParse(campos[1], out sentTotal);
|
|
int.TryParse(campos[3], out visiveis);
|
|
|
|
if (UltimaLeitura.SatelitesEmVista == null)
|
|
{
|
|
UltimaLeitura.SatelitesEmVista =
|
|
new List<GPSSatelitesEmVistaModel>();
|
|
}
|
|
|
|
PenultimaLeitura.Momento = UltimaLeitura.Momento;
|
|
PenultimaLeitura.SatelitesEmVista =
|
|
new List<GPSSatelitesEmVistaModel>(
|
|
UltimaLeitura.SatelitesEmVista
|
|
);
|
|
|
|
UltimaLeitura.Momento = DateTime.Now;
|
|
|
|
GPSSatelitesEmVistaModel leitura =
|
|
UltimaLeitura.SatelitesEmVista
|
|
.FirstOrDefault(x => x.TipoSistema == tipoSistema);
|
|
|
|
if (leitura == null)
|
|
{
|
|
leitura = new GPSSatelitesEmVistaModel
|
|
{
|
|
TipoSistema = tipoSistema,
|
|
Sentencas =
|
|
new List<GPSSatelitesEmVistaSentencaModel>()
|
|
};
|
|
|
|
UltimaLeitura.SatelitesEmVista.Add(leitura);
|
|
}
|
|
|
|
GPSSatelitesEmVistaSentencaModel parte =
|
|
leitura.Sentencas.FirstOrDefault(
|
|
x => x.SentencaAtual == sentAtual
|
|
);
|
|
|
|
if (parte == null)
|
|
{
|
|
parte = new GPSSatelitesEmVistaSentencaModel
|
|
{
|
|
SentencaAtual = sentAtual,
|
|
SentencasTotal = sentTotal,
|
|
QuantidadeSatelites = visiveis,
|
|
Dados =
|
|
new List<GPSSatelitesEmVistaDadosModel>()
|
|
};
|
|
|
|
leitura.Sentencas.Add(parte);
|
|
}
|
|
|
|
parte.Dados =
|
|
new List<GPSSatelitesEmVistaDadosModel>();
|
|
|
|
for (int i = 4; i + 3 < campos.Length; i += 4)
|
|
{
|
|
double elevacao;
|
|
double azimute;
|
|
double snr;
|
|
|
|
double.TryParse(
|
|
campos[i + 1],
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out elevacao
|
|
);
|
|
|
|
double.TryParse(
|
|
campos[i + 2],
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out azimute
|
|
);
|
|
|
|
double.TryParse(
|
|
StripChecksum(campos[i + 3]),
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out snr
|
|
);
|
|
|
|
parte.Dados.Add(
|
|
new GPSSatelitesEmVistaDadosModel
|
|
{
|
|
PRN = campos[i],
|
|
Elevacao = elevacao,
|
|
Azimute = azimute,
|
|
QualidadeSinal = snr
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private static void ProcessarGNTHS(string sentenca)
|
|
{
|
|
string conteudo = sentenca.Trim();
|
|
|
|
if (conteudo.StartsWith("$"))
|
|
conteudo = conteudo.Substring(1);
|
|
|
|
string[] partesChecksum = conteudo.Split(
|
|
new[] { '*' },
|
|
2
|
|
);
|
|
|
|
string[] campos = partesChecksum[0].Split(',');
|
|
|
|
if (campos.Length < 3)
|
|
throw new FormatException("THS incompleta.");
|
|
|
|
string headingTexto =
|
|
(campos[1] ?? string.Empty).Trim();
|
|
|
|
string status =
|
|
(campos[2] ?? string.Empty)
|
|
.Trim()
|
|
.ToUpperInvariant();
|
|
|
|
double headingTrue;
|
|
|
|
bool numerico = double.TryParse(
|
|
headingTexto,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out headingTrue
|
|
);
|
|
|
|
bool valido =
|
|
status == "A" &&
|
|
numerico &&
|
|
!double.IsNaN(headingTrue) &&
|
|
!double.IsInfinity(headingTrue);
|
|
|
|
PenultimaLeitura.TimestampOri.valor =
|
|
UltimaLeitura.TimestampOri.valor;
|
|
|
|
PenultimaLeitura.Momento =
|
|
UltimaLeitura.Momento;
|
|
|
|
PenultimaLeitura.OrientacaoReal =
|
|
UltimaLeitura.OrientacaoReal;
|
|
|
|
PenultimaLeitura.TipoOrientacao =
|
|
UltimaLeitura.TipoOrientacao;
|
|
|
|
UltimaLeitura.TimestampOri.valor =
|
|
Stopwatch.GetTimestamp() /
|
|
(double)Stopwatch.Frequency;
|
|
|
|
UltimaLeitura.Momento = DateTime.Now;
|
|
|
|
if (!valido)
|
|
{
|
|
UltimaLeitura.OrientacaoReal = 9999;
|
|
UltimaLeitura.TipoOrientacao = "V";
|
|
return;
|
|
}
|
|
|
|
headingTrue = GPSUtils.NormalizarAngulo(headingTrue);
|
|
|
|
UltimaLeitura.OrientacaoReal =
|
|
InverterHeading
|
|
? GPSUtils.NormalizarAngulo(
|
|
headingTrue - 180.0
|
|
)
|
|
: headingTrue;
|
|
|
|
UltimaLeitura.TipoOrientacao = "A";
|
|
|
|
AplicarPosicaoCorrigida();
|
|
DefinirAnguloCarro();
|
|
}
|
|
|
|
private static void ProcessarGxGSA(string sentenca)
|
|
{
|
|
string[] campos = sentenca.Split(',');
|
|
|
|
if (campos.Length < 17)
|
|
return;
|
|
|
|
TiposDimensaoCorrecaoGPS modoSolucao =
|
|
(TiposDimensaoCorrecaoGPS)
|
|
GPSUtils.ParseInt(campos[2]);
|
|
|
|
int satsUsados = 0;
|
|
|
|
for (int i = 3; i <= 14 && i < campos.Length; i++)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(campos[i]))
|
|
satsUsados++;
|
|
}
|
|
|
|
double pdop = GPSUtils.ParseDouble(campos[15]);
|
|
double hdop = GPSUtils.ParseDouble(campos[16]);
|
|
|
|
double vdop = campos.Length > 17
|
|
? GPSUtils.ParseDouble(
|
|
StripChecksum(campos[17])
|
|
)
|
|
: double.NaN;
|
|
|
|
if (!double.IsInfinity(hdop) &&
|
|
!double.IsNaN(hdop))
|
|
{
|
|
UltimaLeitura.PrecisaoHorizontal = hdop;
|
|
}
|
|
|
|
if (satsUsados > 0)
|
|
{
|
|
UltimaLeitura.NumeroSatelites =
|
|
Math.Max(
|
|
UltimaLeitura.NumeroSatelites,
|
|
satsUsados
|
|
);
|
|
}
|
|
|
|
if (!double.IsInfinity(pdop) &&
|
|
!double.IsNaN(pdop))
|
|
{
|
|
UltimaLeitura.PDOP = pdop;
|
|
}
|
|
|
|
if (!double.IsInfinity(vdop) &&
|
|
!double.IsNaN(vdop))
|
|
{
|
|
UltimaLeitura.VDOP = vdop;
|
|
}
|
|
|
|
UltimaLeitura.FixDimensao = modoSolucao;
|
|
}
|
|
|
|
private static void ProcessarGxRMC(string sentenca)
|
|
{
|
|
string[] campos = sentenca.Split(',');
|
|
|
|
if (campos.Length < 10)
|
|
throw new FormatException("RMC incompleta.");
|
|
|
|
string timeUtc = GetField(campos, 1);
|
|
string status = GetField(campos, 2);
|
|
string latRaw = GetField(campos, 3);
|
|
string latHem = GetField(campos, 4);
|
|
string lonRaw = GetField(campos, 5);
|
|
string lonHem = GetField(campos, 6);
|
|
string spdKnotsRaw = GetField(campos, 7);
|
|
string cogRaw = GetField(campos, 8);
|
|
string dateRaw = GetField(campos, 9);
|
|
string magRaw = GetField(campos, 10);
|
|
string magHem = StripChecksum(GetField(campos, 11));
|
|
|
|
string mode = campos.Length > 12
|
|
? StripChecksum(campos[12]).Trim()
|
|
: string.Empty;
|
|
|
|
bool valido =
|
|
string.Equals(
|
|
status,
|
|
"A",
|
|
StringComparison.OrdinalIgnoreCase
|
|
);
|
|
|
|
double lat;
|
|
double lon;
|
|
|
|
if (valido &&
|
|
TryParseDmm(latRaw, latHem, 2, out lat) &&
|
|
TryParseDmm(lonRaw, lonHem, 3, out lon))
|
|
{
|
|
PenultimaLeitura.Latitude =
|
|
UltimaLeitura.Latitude;
|
|
|
|
PenultimaLeitura.Longitude =
|
|
UltimaLeitura.Longitude;
|
|
|
|
PenultimaLeitura.LatitudeAnt =
|
|
UltimaLeitura.LatitudeAnt;
|
|
|
|
PenultimaLeitura.LongitudeAnt =
|
|
UltimaLeitura.LongitudeAnt;
|
|
|
|
UltimaLeitura.LatitudeAnt = lat;
|
|
UltimaLeitura.LongitudeAnt = lon;
|
|
|
|
AplicarPosicaoCorrigida();
|
|
}
|
|
|
|
double spdKnots;
|
|
|
|
if (double.TryParse(
|
|
spdKnotsRaw,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out spdKnots))
|
|
{
|
|
/*
|
|
* Mantém a semântica histórica do projeto:
|
|
* o campo recebe o valor da sentença em knots.
|
|
*/
|
|
UltimaLeitura.Velocidade = spdKnots;
|
|
}
|
|
|
|
double cog;
|
|
|
|
if (double.TryParse(
|
|
cogRaw,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out cog))
|
|
{
|
|
UltimaLeitura.CursoVerdadeiro = cog;
|
|
}
|
|
|
|
DateTime dataHora;
|
|
|
|
if (TryParseRmcDateTime(
|
|
dateRaw,
|
|
timeUtc,
|
|
out dataHora))
|
|
{
|
|
UltimaLeitura.DataHora =
|
|
dataHora.ToLocalTime();
|
|
}
|
|
|
|
double mag;
|
|
|
|
if (double.TryParse(
|
|
magRaw,
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out mag))
|
|
{
|
|
if (string.Equals(
|
|
magHem,
|
|
"W",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
mag = -mag;
|
|
}
|
|
|
|
UltimaLeitura.VariacaoMagnetica = mag;
|
|
}
|
|
|
|
AplicarModoRmc(mode);
|
|
AtualizarCoordenadasGPS();
|
|
}
|
|
|
|
private static void AplicarModoRmc(string mode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(mode))
|
|
return;
|
|
|
|
switch (mode.Trim().ToUpperInvariant())
|
|
{
|
|
case "R":
|
|
UltimaLeitura.QualidadeFix =
|
|
TiposCorrecaoGPS.RTKFixo;
|
|
break;
|
|
|
|
case "F":
|
|
UltimaLeitura.QualidadeFix =
|
|
TiposCorrecaoGPS.RTKFlutuante;
|
|
break;
|
|
|
|
case "D":
|
|
UltimaLeitura.QualidadeFix =
|
|
TiposCorrecaoGPS.DGPS;
|
|
break;
|
|
|
|
case "E":
|
|
UltimaLeitura.QualidadeFix =
|
|
TiposCorrecaoGPS.DeadReckoing;
|
|
break;
|
|
|
|
case "A":
|
|
UltimaLeitura.QualidadeFix =
|
|
TiposCorrecaoGPS.Autonomo;
|
|
break;
|
|
|
|
case "N":
|
|
UltimaLeitura.QualidadeFix =
|
|
TiposCorrecaoGPS.SemCorrecao;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// RTCM VIA MQTT / FILA DE ESCRITA
|
|
// ============================================================
|
|
|
|
public static void AplicarCorrecaoRTK_Mqtt(byte[] correcao, int bytesRead)
|
|
{
|
|
if (correcao == null || bytesRead <= 0 || bytesRead > correcao.Length || bytesRead > RtcmMaxPayloadBytes)
|
|
{
|
|
Interlocked.Increment(ref _rtcmDroppedInvalid);
|
|
return;
|
|
}
|
|
|
|
byte[] copia = new byte[bytesRead];
|
|
Buffer.BlockCopy(correcao, 0, copia, 0, bytesRead);
|
|
|
|
EnfileirarRtcm(copia, RtcmSource.Mqtt);
|
|
}
|
|
|
|
private static void EnfileirarRtcm(byte[] bytes, RtcmSource source)
|
|
{
|
|
EnsureBackgroundWorkersStarted();
|
|
|
|
long agora = Stopwatch.GetTimestamp();
|
|
|
|
Interlocked.Increment(ref _rtcmReceived);
|
|
Interlocked.Exchange(ref _lastRtcmReceivedMono, agora);
|
|
|
|
var envelope = new RtcmEnvelope
|
|
{
|
|
Bytes = bytes,
|
|
ReceivedMonotonic = agora,
|
|
ReceivedUtc = DateTime.UtcNow,
|
|
Sequence = Interlocked.Increment(
|
|
ref _rtcmSequence
|
|
),
|
|
Source = source,
|
|
MessageType = TryGetRtcmMessageType(bytes)
|
|
};
|
|
|
|
lock (_rtcmQueueLock)
|
|
{
|
|
while (_rtcmQueue.Count >= RtcmQueueCapacity)
|
|
{
|
|
_rtcmQueue.Dequeue();
|
|
Interlocked.Increment(
|
|
ref _rtcmDroppedQueue
|
|
);
|
|
}
|
|
|
|
_rtcmQueue.Enqueue(envelope);
|
|
Interlocked.Increment(ref _rtcmQueued);
|
|
}
|
|
|
|
try { _rtcmSignal.Release(); }
|
|
catch (SemaphoreFullException) { }
|
|
}
|
|
|
|
private static async Task RtcmWriterLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await _rtcmSignal.WaitAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
RtcmEnvelope envelope = null;
|
|
|
|
lock (_rtcmQueueLock)
|
|
{
|
|
if (_rtcmQueue.Count > 0)
|
|
envelope = _rtcmQueue.Dequeue();
|
|
}
|
|
|
|
if (envelope == null)
|
|
break;
|
|
|
|
double idadeMs = GetAgeMs(
|
|
envelope.ReceivedMonotonic
|
|
);
|
|
|
|
if (idadeMs > RtcmMaxAgeMs)
|
|
{
|
|
Interlocked.Increment(
|
|
ref _rtcmDroppedStale
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (!Iniciado)
|
|
{
|
|
Interlocked.Increment(
|
|
ref _rtcmDroppedQueue
|
|
);
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
await _serialWriteLock
|
|
.WaitAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
/*
|
|
* Verifica novamente depois de esperar o lock.
|
|
* A configuração pode ter ocupado a serial.
|
|
*/
|
|
idadeMs = GetAgeMs(
|
|
envelope.ReceivedMonotonic
|
|
);
|
|
|
|
if (idadeMs > RtcmMaxAgeMs)
|
|
{
|
|
Interlocked.Increment(
|
|
ref _rtcmDroppedStale
|
|
);
|
|
continue;
|
|
}
|
|
|
|
WriteSerialUnsafe(
|
|
envelope.Bytes,
|
|
envelope.Bytes.Length
|
|
);
|
|
|
|
UltimoEnvioCorrecaoRTK =
|
|
DateTime.Now;
|
|
|
|
Interlocked.Exchange(
|
|
ref _lastRtcmWrittenMono,
|
|
Stopwatch.GetTimestamp()
|
|
);
|
|
|
|
Interlocked.Increment(
|
|
ref _rtcmWritten
|
|
);
|
|
}
|
|
finally
|
|
{
|
|
_serialWriteLock.Release();
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Interlocked.Increment(
|
|
ref _rtcmWriteErrors
|
|
);
|
|
|
|
RecordError(
|
|
"[GPSService.RtcmWriter] " +
|
|
ex.Message
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static int TryGetRtcmMessageType(byte[] bytes)
|
|
{
|
|
/*
|
|
* RTCM3:
|
|
* D3 | 6 bits reservados + 10 bits length | payload
|
|
* Os 12 primeiros bits do payload formam o message type.
|
|
*/
|
|
if (bytes == null ||
|
|
bytes.Length < 5 ||
|
|
bytes[0] != 0xD3)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
return ((bytes[3] << 4) | (bytes[4] >> 4))
|
|
& 0x0FFF;
|
|
}
|
|
|
|
// ============================================================
|
|
// NTRIP
|
|
// ============================================================
|
|
|
|
public static async Task IniciarNtripAsync()
|
|
{
|
|
if (!Variaveis.IsAgroBase || !CorrecaoRTK_Ntrip)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_workerLock)
|
|
{
|
|
if (_ntripTask != null &&
|
|
!_ntripTask.IsCompleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ntripCts = new CancellationTokenSource();
|
|
_ntripTask = Task.Run(
|
|
() => AplicarCorrecaoRTK_Ntrip(
|
|
_ntripCts.Token
|
|
)
|
|
);
|
|
}
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
public static async Task PararNtripAsync()
|
|
{
|
|
CancellationTokenSource cts;
|
|
Task task;
|
|
TcpClient client;
|
|
|
|
lock (_workerLock)
|
|
{
|
|
cts = _ntripCts;
|
|
task = _ntripTask;
|
|
client = _ntripClient;
|
|
|
|
_ntripCts = null;
|
|
_ntripTask = null;
|
|
_ntripClient = null;
|
|
}
|
|
|
|
CorrecaoRTK_Ntrip = false;
|
|
|
|
if (cts != null)
|
|
{
|
|
try { cts.Cancel(); } catch { }
|
|
}
|
|
|
|
if (client != null)
|
|
{
|
|
try { client.Close(); } catch { }
|
|
try { client.Dispose(); } catch { }
|
|
}
|
|
|
|
if (task != null)
|
|
{
|
|
try { await task.ConfigureAwait(false); }
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.PararNtripAsync] " +
|
|
ex.Message
|
|
);
|
|
}
|
|
}
|
|
|
|
if (cts != null)
|
|
cts.Dispose();
|
|
}
|
|
|
|
private static async Task AplicarCorrecaoRTK_Ntrip(CancellationToken cancellationToken)
|
|
{
|
|
if (!Iniciado || LoopRTK_Ntrip || !APIService.HasInternet)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LoopRTK_Ntrip = true;
|
|
|
|
string host = Environment.GetEnvironmentVariable(
|
|
"AGRO_NTRIP_HOST"
|
|
) ?? "gps-ntrip.ibge.gov.br";
|
|
|
|
int port = 2101;
|
|
int portEnv;
|
|
|
|
if (int.TryParse(
|
|
Environment.GetEnvironmentVariable(
|
|
"AGRO_NTRIP_PORT"
|
|
),
|
|
out portEnv))
|
|
{
|
|
port = portEnv;
|
|
}
|
|
|
|
string mountpoint =
|
|
Environment.GetEnvironmentVariable(
|
|
"AGRO_NTRIP_MOUNTPOINT"
|
|
) ?? "EESC0";
|
|
|
|
string username = "Zendion";
|
|
//Environment.GetEnvironmentVariable(
|
|
// "AGRO_NTRIP_USERNAME"
|
|
//) ?? string.Empty;
|
|
|
|
string password = "QD&m1p60";
|
|
//Environment.GetEnvironmentVariable(
|
|
// "AGRO_NTRIP_PASSWORD"
|
|
//) ?? string.Empty;
|
|
|
|
int backoffMs = 1000;
|
|
var random = new Random();
|
|
|
|
try
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested &&
|
|
CorrecaoRTK_Ntrip &&
|
|
APIService.HasInternet)
|
|
{
|
|
TcpClient client = null;
|
|
|
|
try
|
|
{
|
|
client = new TcpClient();
|
|
|
|
lock (_workerLock)
|
|
_ntripClient = client;
|
|
|
|
Task connectTask =
|
|
client.ConnectAsync(host, port);
|
|
|
|
Task completed = await Task.WhenAny(
|
|
connectTask,
|
|
Task.Delay(
|
|
10000,
|
|
cancellationToken
|
|
)
|
|
).ConfigureAwait(false);
|
|
|
|
if (completed != connectTask)
|
|
throw new TimeoutException(
|
|
"Timeout ao conectar ao NTRIP."
|
|
);
|
|
|
|
await connectTask.ConfigureAwait(false);
|
|
|
|
NetworkStream stream =
|
|
client.GetStream();
|
|
|
|
string credentials =
|
|
string.IsNullOrEmpty(username)
|
|
? string.Empty
|
|
: Convert.ToBase64String(
|
|
Encoding.ASCII.GetBytes(
|
|
username + ":" + password
|
|
)
|
|
);
|
|
|
|
string request =
|
|
"GET /" + mountpoint +
|
|
" HTTP/1.0\r\n" +
|
|
"User-Agent: NTRIP AgroBase/2.0\r\n" +
|
|
"Accept: */*\r\n" +
|
|
"Connection: close\r\n" +
|
|
(
|
|
string.IsNullOrEmpty(credentials)
|
|
? string.Empty
|
|
: "Authorization: Basic " +
|
|
credentials + "\r\n"
|
|
) +
|
|
"\r\n";
|
|
|
|
byte[] requestBytes =
|
|
Encoding.ASCII.GetBytes(request);
|
|
|
|
await stream.WriteAsync(
|
|
requestBytes,
|
|
0,
|
|
requestBytes.Length,
|
|
cancellationToken
|
|
).ConfigureAwait(false);
|
|
|
|
NtripHeaderResult header =
|
|
await ReadNtripHeaderAsync(
|
|
stream,
|
|
cancellationToken
|
|
).ConfigureAwait(false);
|
|
|
|
if (!header.Success)
|
|
{
|
|
throw new IOException(
|
|
"Resposta NTRIP inválida: " +
|
|
header.StatusLine
|
|
);
|
|
}
|
|
|
|
Variaveis.MostrarLog(
|
|
"[GPSService.NTRIP] Conectado a " +
|
|
host + "/" + mountpoint
|
|
);
|
|
|
|
backoffMs = 1000;
|
|
|
|
if (header.RemainingBytes != null &&
|
|
header.RemainingBytes.Length > 0)
|
|
{
|
|
Interlocked.Add(
|
|
ref _ntripBytesReceived,
|
|
header.RemainingBytes.Length
|
|
);
|
|
|
|
EnfileirarRtcm(
|
|
header.RemainingBytes,
|
|
RtcmSource.Ntrip
|
|
);
|
|
}
|
|
|
|
byte[] buffer = new byte[4096];
|
|
|
|
while (!cancellationToken
|
|
.IsCancellationRequested &&
|
|
CorrecaoRTK_Ntrip)
|
|
{
|
|
int bytesRead = await stream.ReadAsync(
|
|
buffer,
|
|
0,
|
|
buffer.Length,
|
|
cancellationToken
|
|
).ConfigureAwait(false);
|
|
|
|
if (bytesRead <= 0)
|
|
break;
|
|
|
|
byte[] bloco = new byte[bytesRead];
|
|
|
|
Buffer.BlockCopy(
|
|
buffer,
|
|
0,
|
|
bloco,
|
|
0,
|
|
bytesRead
|
|
);
|
|
|
|
Interlocked.Add(
|
|
ref _ntripBytesReceived,
|
|
bytesRead
|
|
);
|
|
|
|
EnfileirarRtcm(
|
|
bloco,
|
|
RtcmSource.Ntrip
|
|
);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Interlocked.Increment(
|
|
ref _ntripErrors
|
|
);
|
|
|
|
RecordError(
|
|
"[GPSService.NTRIP] " + ex.Message
|
|
);
|
|
}
|
|
finally
|
|
{
|
|
lock (_workerLock)
|
|
{
|
|
if (ReferenceEquals(
|
|
_ntripClient,
|
|
client))
|
|
{
|
|
_ntripClient = null;
|
|
}
|
|
}
|
|
|
|
if (client != null)
|
|
{
|
|
try { client.Close(); } catch { }
|
|
try { client.Dispose(); } catch { }
|
|
}
|
|
}
|
|
|
|
if (cancellationToken.IsCancellationRequested ||
|
|
!CorrecaoRTK_Ntrip)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Interlocked.Increment(ref _ntripReconnects);
|
|
|
|
int jitter = random.Next(0, 350);
|
|
|
|
await Task.Delay(
|
|
backoffMs + jitter,
|
|
cancellationToken
|
|
).ConfigureAwait(false);
|
|
|
|
backoffMs = Math.Min(
|
|
30000,
|
|
(int)(backoffMs * 1.8)
|
|
);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
LoopRTK_Ntrip = false;
|
|
}
|
|
}
|
|
|
|
private static async Task<NtripHeaderResult> ReadNtripHeaderAsync(NetworkStream stream, CancellationToken cancellationToken)
|
|
{
|
|
var bytes = new List<byte>(2048);
|
|
byte[] buffer = new byte[512];
|
|
|
|
while (bytes.Count < 16 * 1024)
|
|
{
|
|
int read = await stream.ReadAsync(
|
|
buffer,
|
|
0,
|
|
buffer.Length,
|
|
cancellationToken
|
|
).ConfigureAwait(false);
|
|
|
|
if (read <= 0)
|
|
break;
|
|
|
|
for (int i = 0; i < read; i++)
|
|
bytes.Add(buffer[i]);
|
|
|
|
int headerEnd = FindHeaderEnd(bytes);
|
|
|
|
if (headerEnd >= 0)
|
|
{
|
|
byte[] all = bytes.ToArray();
|
|
string headerText = Encoding.ASCII.GetString(
|
|
all,
|
|
0,
|
|
headerEnd
|
|
);
|
|
|
|
string statusLine = headerText
|
|
.Split(new[] { "\r\n" },
|
|
StringSplitOptions.None)
|
|
.FirstOrDefault() ?? string.Empty;
|
|
|
|
bool success =
|
|
statusLine.IndexOf(
|
|
"200",
|
|
StringComparison.OrdinalIgnoreCase
|
|
) >= 0 ||
|
|
statusLine.StartsWith(
|
|
"ICY 200",
|
|
StringComparison.OrdinalIgnoreCase
|
|
);
|
|
|
|
int payloadStart = headerEnd + 4;
|
|
int remainingLength =
|
|
all.Length - payloadStart;
|
|
|
|
byte[] remaining =
|
|
remainingLength > 0
|
|
? all.Skip(payloadStart)
|
|
.Take(remainingLength)
|
|
.ToArray()
|
|
: new byte[0];
|
|
|
|
return new NtripHeaderResult
|
|
{
|
|
Success = success,
|
|
StatusLine = statusLine,
|
|
RemainingBytes = remaining
|
|
};
|
|
}
|
|
}
|
|
|
|
return new NtripHeaderResult
|
|
{
|
|
Success = false,
|
|
StatusLine = "Cabeçalho NTRIP incompleto.",
|
|
RemainingBytes = new byte[0]
|
|
};
|
|
}
|
|
|
|
private static int FindHeaderEnd(List<byte> bytes)
|
|
{
|
|
for (int i = 3; i < bytes.Count; i++)
|
|
{
|
|
if (bytes[i - 3] == 13 &&
|
|
bytes[i - 2] == 10 &&
|
|
bytes[i - 1] == 13 &&
|
|
bytes[i] == 10)
|
|
{
|
|
return i - 3;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// ============================================================
|
|
// SNAPSHOT E ESTADO GNSS
|
|
// ============================================================
|
|
|
|
public static GPSModel GetSnapshot()
|
|
{
|
|
lock (_stateLock)
|
|
return UltimaLeitura.Clone();
|
|
}
|
|
|
|
public static GPSModel GetPreviousSnapshot()
|
|
{
|
|
lock (_stateLock)
|
|
return PenultimaLeitura.Clone();
|
|
}
|
|
|
|
private static bool PossuiHeadingValido(GPSModel leitura)
|
|
{
|
|
if (leitura == null)
|
|
return false;
|
|
|
|
string status =
|
|
(leitura.TipoOrientacao ?? string.Empty)
|
|
.Trim()
|
|
.ToUpperInvariant();
|
|
|
|
double heading = leitura.OrientacaoReal;
|
|
|
|
return
|
|
status == "A" &&
|
|
leitura.TimestampOri.frequencia >= 1.0 &&
|
|
!double.IsNaN(heading) &&
|
|
!double.IsInfinity(heading) &&
|
|
heading >= 0.0 &&
|
|
heading < 360.0;
|
|
}
|
|
|
|
private static void AplicarPosicaoCorrigida()
|
|
{
|
|
if (!PossuiHeadingValido(UltimaLeitura))
|
|
{
|
|
UltimaLeitura.Latitude =
|
|
UltimaLeitura.LatitudeAnt;
|
|
|
|
UltimaLeitura.Longitude =
|
|
UltimaLeitura.LongitudeAnt;
|
|
|
|
return;
|
|
}
|
|
|
|
var corrigida =
|
|
LeverArm.FixLeverArmLatLon_Fast(
|
|
UltimaLeitura.LatitudeAnt,
|
|
UltimaLeitura.LongitudeAnt,
|
|
UltimaLeitura.OrientacaoReal,
|
|
UltimaLeitura.TimestampOri.frequencia
|
|
);
|
|
|
|
UltimaLeitura.Latitude = corrigida.lat;
|
|
UltimaLeitura.Longitude = corrigida.lon;
|
|
}
|
|
|
|
public static void AtualizarCoordenadasGPS()
|
|
{
|
|
if (!Variaveis.IsAgroBase)
|
|
return;
|
|
|
|
var op = Variaveis.OperacaoEmAndamento;
|
|
|
|
PenultimaLeitura.Ntrip_ativado = UltimaLeitura.Ntrip_ativado;
|
|
PenultimaLeitura.Heartbeat = UltimaLeitura.Heartbeat;
|
|
PenultimaLeitura.LeverArmFrontal = UltimaLeitura.LeverArmFrontal;
|
|
PenultimaLeitura.LeverArmLateral = UltimaLeitura.LeverArmLateral;
|
|
PenultimaLeitura.LeverArmFrontalOp = UltimaLeitura.LeverArmFrontalOp;
|
|
PenultimaLeitura.LeverArmLateralOp = UltimaLeitura.LeverArmLateralOp;
|
|
UltimaLeitura.Ntrip_ativado = CorrecaoRTK_Ntrip;
|
|
|
|
UltimaLeitura.Heartbeat = (UltimaLeitura.Heartbeat + 1) % 10;
|
|
UltimaLeitura.LeverArmFrontal = LeverArm != null ? LeverArm.FrontalTotalCm : 0;
|
|
UltimaLeitura.LeverArmLateral = LeverArm != null ? LeverArm.LateralTotalCm : 0;
|
|
UltimaLeitura.LeverArmFrontalOp = LeverArm != null ? LeverArm.FrontalCampoCm : 0;
|
|
UltimaLeitura.LeverArmLateralOp = LeverArm != null ? LeverArm.LateralCampoCm : 0;
|
|
|
|
UltimasLeituras.Add(UltimaLeitura.Clone());
|
|
|
|
while (UltimasLeituras.Count > Math.Max(1, TaxaAmostragemHz))
|
|
{
|
|
UltimasLeituras.RemoveAt(0);
|
|
}
|
|
|
|
DefinirOrientacaoMovimento();
|
|
AtualizaDadosRedis();
|
|
|
|
if (op.Sensoriamento.Operacao.OperacaoIniciada)
|
|
{
|
|
var trajetoria = op.Trajetoria;
|
|
var gpsTrajetoria = op.GPSTrajetoria;
|
|
GPSModel novaLeitura = UltimaLeitura.Clone();
|
|
|
|
gpsTrajetoria.Add(novaLeitura);
|
|
|
|
if (gpsTrajetoria.Count > 1)
|
|
{
|
|
int ultimo = gpsTrajetoria.Count - 1;
|
|
int penultimo = ultimo - 1;
|
|
double distancia = GPSUtils.DistanciaEntrePontos(gpsTrajetoria[ultimo], gpsTrajetoria[penultimo]);
|
|
|
|
if (trajetoria != null)
|
|
trajetoria.AtualizarDistanciaPercorrida(distancia);
|
|
}
|
|
|
|
var bicos = op.DispAtu?.Dados?.BicosPulverizadores;
|
|
|
|
if (bicos != null)
|
|
{
|
|
foreach (var bico in bicos)
|
|
{
|
|
if (!bico.Inicializado || !bico.Comandar || !bico.ComandoAtuar)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bico.AdicionarPontoTrechoAtivo(UltimaLeitura.Latitude, UltimaLeitura.Longitude);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (op.Trajetoria != null)
|
|
{
|
|
op.Trajetoria.LoopAtualizaDados();
|
|
}
|
|
|
|
AtualizarTrajetoriaDinamica();
|
|
|
|
int enderecoEquipamento = 0x02;
|
|
|
|
EnviarCoordenadasParaMapa(UltimaLeitura.Latitude, UltimaLeitura.Longitude, UltimaLeitura.AnguloCarroDefinido, op.Sensoriamento.Operacao.OperacaoIniciada, enderecoEquipamento);
|
|
|
|
GPSModel posicaoBase = VariaveisOperacao.ObterPosicaoBaseSnapshot();
|
|
if ((posicaoBase?.UltimoComandoRespondido ?? DateTime.MinValue) > DateTime.UtcNow.AddSeconds(-60))
|
|
{
|
|
int enderecoBase = 0x01;
|
|
EnviarCoordenadasParaMapa(posicaoBase.Latitude, posicaoBase.Longitude, posicaoBase.OrientacaoReal, false, enderecoBase);
|
|
}
|
|
|
|
PenultimaLeitura.Inicializado = UltimaLeitura.Inicializado;
|
|
UltimaLeitura.Inicializado = Iniciado;
|
|
|
|
if (CorrecaoRTK_Ntrip && GetAgeMs(Interlocked.Read(ref _lastRtcmWrittenMono)) > TempoMin_Ntrip * 1000.0)
|
|
{
|
|
_ = IniciarNtripAsync();
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// PUBLICAÇÃO LOCAL LATEST-ONLY
|
|
// ============================================================
|
|
|
|
public static void EnviarCoordenadasParaMapa(double Latitude, double Longitude, double Orientacao, bool EmFoco, int ID)
|
|
{
|
|
var coordenadas = new
|
|
{
|
|
latitude = Latitude,
|
|
longitude = Longitude,
|
|
orientacao = GPSUtils.NormalizarAngulo(Orientacao - 180.0),
|
|
id = ID,
|
|
foco = EmFoco
|
|
};
|
|
|
|
QueueLocalPublication(MapasVariaveisModel.TopicoCoordenadasGPS, JsonConvert.SerializeObject(coordenadas));
|
|
}
|
|
|
|
public static void AtualizarTrajetoriaDinamica()
|
|
{
|
|
var op = Variaveis.OperacaoEmAndamento;
|
|
|
|
var dinamica = op.Trajetoria?._TrajetoriaDinamica;
|
|
|
|
if (!(dinamica?.Any() ?? false))
|
|
return;
|
|
|
|
var trajetoria = op.Trajetoria.TrajetoriaDinamica.Select(x => new
|
|
{
|
|
latitude = x.Latitude,
|
|
longitude = x.Longitude
|
|
})
|
|
.ToArray();
|
|
|
|
QueueLocalPublication(MapasVariaveisModel.TopicoTrajetoriaDinamica, JsonConvert.SerializeObject(trajetoria));
|
|
}
|
|
|
|
public static void AtualizarRuasSelecionadas(List<string> RuasSelecionadas)
|
|
{
|
|
/*
|
|
* Preserva o formato legado produzido anteriormente.
|
|
*/
|
|
QueueLocalPublication(
|
|
MapasVariaveisModel
|
|
.TopicoSelecaoRuasMapa,
|
|
JsonConvert.SerializeObject(
|
|
"[" +
|
|
string.Join(
|
|
",",
|
|
(RuasSelecionadas ??
|
|
new List<string>()).ToArray()
|
|
) +
|
|
"]"
|
|
)
|
|
);
|
|
}
|
|
|
|
private static void QueueLocalPublication(string topic, string payload)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(topic))
|
|
return;
|
|
|
|
EnsureBackgroundWorkersStarted();
|
|
|
|
lock (_localPublishLock)
|
|
{
|
|
_localLatestPayload[topic] =
|
|
payload ?? string.Empty;
|
|
}
|
|
|
|
try { _localPublishSignal.Release(); }
|
|
catch (SemaphoreFullException) { }
|
|
}
|
|
|
|
private static async Task LocalPublisherLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await _localPublishSignal
|
|
.WaitAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Dictionary<string, string> lote;
|
|
|
|
lock (_localPublishLock)
|
|
{
|
|
lote = new Dictionary<string, string>(
|
|
_localLatestPayload,
|
|
StringComparer.Ordinal
|
|
);
|
|
|
|
_localLatestPayload.Clear();
|
|
}
|
|
|
|
MqttService mqtt =
|
|
Variaveis.MqttServiceLocal;
|
|
|
|
if (mqtt == null ||
|
|
!mqtt.StatusConexao())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var item in lote)
|
|
{
|
|
if (cancellationToken
|
|
.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MqttService.MqttTopicosModel topico =
|
|
mqtt.Topicos.FirstOrDefault(
|
|
x => x.Topico == item.Key
|
|
);
|
|
|
|
if (topico == null)
|
|
continue;
|
|
|
|
try
|
|
{
|
|
await mqtt.PublishAsync(
|
|
topico,
|
|
item.Value,
|
|
true
|
|
).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.LocalPublisher] " +
|
|
ex.Message
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// ORIENTAÇÃO, REDIS E HISTÓRICO
|
|
// ============================================================
|
|
|
|
private static void DefinirOrientacaoMovimento()
|
|
{
|
|
if (UltimasLeituras.Count < 2)
|
|
{
|
|
double ang = GPSUtils.CalcularOrientacao(PenultimaLeitura, UltimaLeitura);
|
|
double d = GPSUtils.DistanciaEntrePontos(PenultimaLeitura, UltimaLeitura);
|
|
PenultimaLeitura.OrientacaoMovimento = UltimaLeitura.OrientacaoMovimento;
|
|
PenultimaLeitura.Distancia = UltimaLeitura.Distancia;
|
|
|
|
UltimaLeitura.OrientacaoMovimento = ang;
|
|
UltimaLeitura.Distancia = d;
|
|
}
|
|
else
|
|
{
|
|
double sumX = 0;
|
|
double sumY = 0;
|
|
double distAcum = 0;
|
|
|
|
for (int i = 0; i < UltimasLeituras.Count - 1; i++)
|
|
{
|
|
GPSModel a = UltimasLeituras[i];
|
|
GPSModel b = UltimasLeituras[i + 1];
|
|
|
|
double angSeg = GPSUtils.CalcularOrientacao(a, b);
|
|
|
|
double dSeg = GPSUtils.DistanciaEntrePontos(a, b);
|
|
|
|
if (dSeg <= 0)
|
|
continue;
|
|
|
|
double rad = angSeg * Math.PI / 180.0;
|
|
|
|
sumX += Math.Cos(rad) * dSeg;
|
|
sumY += Math.Sin(rad) * dSeg;
|
|
distAcum += dSeg;
|
|
}
|
|
|
|
double anguloMovimento;
|
|
|
|
if (distAcum <= 0)
|
|
{
|
|
anguloMovimento = UltimaLeitura.OrientacaoReal;
|
|
}
|
|
else
|
|
{
|
|
anguloMovimento = Math.Atan2(sumY, sumX) * 180.0 / Math.PI;
|
|
anguloMovimento = GPSUtils.NormalizarAngulo(anguloMovimento);
|
|
}
|
|
|
|
GPSModel first = UltimasLeituras[0];
|
|
GPSModel last = UltimasLeituras[UltimasLeituras.Count - 1];
|
|
|
|
double distLinear = GPSUtils.DistanciaEntrePontos(first, last);
|
|
|
|
PenultimaLeitura.OrientacaoMovimento = UltimaLeitura.OrientacaoMovimento;
|
|
PenultimaLeitura.Distancia = UltimaLeitura.Distancia;
|
|
UltimaLeitura.OrientacaoMovimento = anguloMovimento;
|
|
UltimaLeitura.Distancia = distLinear;
|
|
}
|
|
|
|
DefinirAnguloCarro();
|
|
}
|
|
|
|
public static void DefinirAnguloCarro()
|
|
{
|
|
var op = Variaveis.OperacaoEmAndamento;
|
|
|
|
double anguloFinal;
|
|
|
|
bool imuIniciado = op.Sensoriamento?.IMU?.Iniciado ?? false;
|
|
|
|
bool gpsIniciado = Iniciado;
|
|
|
|
bool headingValido = PossuiHeadingValido(UltimaLeitura);
|
|
|
|
if (op.Simulando)
|
|
{
|
|
anguloFinal = UltimaLeitura.OrientacaoReal;
|
|
}
|
|
else if (gpsIniciado && headingValido)
|
|
{
|
|
anguloFinal = UltimaLeitura.OrientacaoReal;
|
|
}
|
|
else if (gpsIniciado && UltimaLeitura.Distancia > 0.25)
|
|
{
|
|
anguloFinal = UltimaLeitura.OrientacaoMovimento;
|
|
}
|
|
else if (imuIniciado)
|
|
{
|
|
anguloFinal = op.Sensoriamento.IMU.YawSeguro;
|
|
}
|
|
else
|
|
{
|
|
anguloFinal = UltimaLeitura.AnguloCarroDefinido;
|
|
}
|
|
|
|
UltimaLeitura.AnguloCarroDefinido = anguloFinal;
|
|
}
|
|
|
|
public static double FundirHeadingComMovimento(double headingDeg, double angMovDeg, double distLinearJanela, double velPercent = 0.0, bool rtkFix = true, double angFundidoAnterior = double.NaN, double alfaLowPass = 0.25)
|
|
{
|
|
double distMin = 0.03;
|
|
double distFull = 0.25;
|
|
double pesoMinHeading = 0.30;
|
|
double fatorConfMovSemFix = 0.80;
|
|
|
|
double fd = Smoothstep(Norm(distLinearJanela, distMin, distFull));
|
|
|
|
double fv =
|
|
FuncoesMatematicas.Clamp(
|
|
velPercent / 100.0,
|
|
0.0,
|
|
1.0
|
|
);
|
|
|
|
double confMov = Math.Max(fd, fv);
|
|
|
|
if (!rtkFix)
|
|
confMov *= fatorConfMovSemFix;
|
|
|
|
double pesoHeading = 1.0 - confMov;
|
|
double minHeading =
|
|
pesoMinHeading * confMov;
|
|
|
|
if (pesoHeading < minHeading)
|
|
pesoHeading = minHeading;
|
|
|
|
if (pesoHeading > 1.0)
|
|
pesoHeading = 1.0;
|
|
|
|
double angMisto =
|
|
MisturarAngulosCircular(
|
|
headingDeg,
|
|
angMovDeg,
|
|
pesoHeading
|
|
);
|
|
|
|
if (!double.IsNaN(angFundidoAnterior) &&
|
|
alfaLowPass > 0)
|
|
{
|
|
angMisto = LowPassAngle0to360(
|
|
angFundidoAnterior,
|
|
angMisto,
|
|
alfaLowPass
|
|
);
|
|
}
|
|
|
|
return Normalize0To360(angMisto);
|
|
}
|
|
|
|
private static double Norm(double value, double min, double max)
|
|
{
|
|
if (max <= min)
|
|
return value >= max ? 1.0 : 0.0;
|
|
|
|
return FuncoesMatematicas.Clamp(
|
|
(value - min) / (max - min),
|
|
0.0,
|
|
1.0
|
|
);
|
|
}
|
|
|
|
private static double Smoothstep(double value)
|
|
{
|
|
double x =
|
|
FuncoesMatematicas.Clamp(
|
|
value,
|
|
0.0,
|
|
1.0
|
|
);
|
|
|
|
return x * x * (3.0 - 2.0 * x);
|
|
}
|
|
|
|
private static double MisturarAngulosCircular(double aDeg, double bDeg, double pesoA)
|
|
{
|
|
double a = aDeg * Math.PI / 180.0;
|
|
double b = bDeg * Math.PI / 180.0;
|
|
|
|
double x =
|
|
Math.Cos(a) * pesoA +
|
|
Math.Cos(b) * (1.0 - pesoA);
|
|
|
|
double y =
|
|
Math.Sin(a) * pesoA +
|
|
Math.Sin(b) * (1.0 - pesoA);
|
|
|
|
return Math.Atan2(y, x) *
|
|
180.0 / Math.PI;
|
|
}
|
|
|
|
private static double LowPassAngle0to360(double atualDeg, double novoDeg, double alfa)
|
|
{
|
|
double diff = NormalizeSigned180(novoDeg - atualDeg);
|
|
|
|
return Normalize0To360(atualDeg + alfa * diff);
|
|
}
|
|
|
|
private static double NormalizeSigned180(double angDeg)
|
|
{
|
|
angDeg = (angDeg + 180.0) % 360.0;
|
|
|
|
if (angDeg < 0)
|
|
angDeg += 360.0;
|
|
|
|
return angDeg - 180.0;
|
|
}
|
|
|
|
private static double Normalize0To360(double angDeg)
|
|
{
|
|
angDeg %= 360.0;
|
|
|
|
if (angDeg < 0)
|
|
angDeg += 360.0;
|
|
|
|
return angDeg;
|
|
}
|
|
|
|
public static void AtualizaDadosRedis()
|
|
{
|
|
try
|
|
{
|
|
GPSModel posicaoAtual;
|
|
|
|
if (Variaveis.OperacaoEmAndamento?.Simulando == true)
|
|
{
|
|
lock (_stateLock)
|
|
{
|
|
posicaoAtual = historicoPosicao.Count > 0 ? historicoPosicao.Peek().Clone() : UltimaLeitura.Clone();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
posicaoAtual = GetSnapshot();
|
|
}
|
|
|
|
bool rtkValido = posicaoAtual.IdadeCorrecao >= 0 && posicaoAtual.IdadeCorrecao < rtk_timeout &&
|
|
(
|
|
posicaoAtual.QualidadeFix == TiposCorrecaoGPS.RTKFixo ||
|
|
posicaoAtual.QualidadeFix == TiposCorrecaoGPS.RTKFlutuante ||
|
|
posicaoAtual.QualidadeFix == TiposCorrecaoGPS.DGPS
|
|
);
|
|
|
|
string statusOrientacao = (posicaoAtual.TipoOrientacao ?? string.Empty).Trim().ToUpperInvariant();
|
|
double valorOrientacao = posicaoAtual.OrientacaoReal;
|
|
bool orientacaoValida = statusOrientacao == "A" && !double.IsNaN(valorOrientacao) && !double.IsInfinity(valorOrientacao) && valorOrientacao >= 0.0 && valorOrientacao < 360.0;
|
|
|
|
GpsTransportMetrics metrics = GetTransportMetrics();
|
|
|
|
RedisService.AtualizarCampos(
|
|
RedisService.ModKey(T_Code.Gps),
|
|
|
|
("conectado", Iniciado),
|
|
("freq_base", TaxaAmostragemHz),
|
|
|
|
("lat", posicaoAtual.Latitude),
|
|
("lon", posicaoAtual.Longitude),
|
|
("theta", posicaoAtual.AnguloCarroDefinido),
|
|
|
|
("fix", (int)posicaoAtual.QualidadeFix),
|
|
("rtk", rtkValido),
|
|
("hAcc", posicaoAtual.PrecisaoCm),
|
|
("nSatelites", posicaoAtual.NumeroSatelites),
|
|
("age", posicaoAtual.IdadeCorrecao),
|
|
|
|
("freq.posicao", posicaoAtual.TimestampPos.frequencia),
|
|
("freq.orientacao", posicaoAtual.TimestampOri.frequencia),
|
|
|
|
("latency.posicao", posicaoAtual.TimestampPos.dt),
|
|
("latency.orientacao", posicaoAtual.TimestampOri.dt),
|
|
|
|
("timestamp.posicao", posicaoAtual.TimestampPos.valor),
|
|
("timestamp.orientacao", posicaoAtual.TimestampOri.valor),
|
|
|
|
("orientacao.status", statusOrientacao),
|
|
("orientacao.valida", orientacaoValida),
|
|
("orientacao.valor", valorOrientacao),
|
|
|
|
("heartbeat", posicaoAtual.Heartbeat),
|
|
|
|
("rtcm.received", metrics.RtcmReceived),
|
|
("rtcm.written", metrics.RtcmWritten),
|
|
("rtcm.queue_depth", metrics.RtcmQueueDepth),
|
|
("rtcm.last_rx_age_ms", metrics.LastRtcmReceivedAgeMs),
|
|
("rtcm.last_write_age_ms", metrics.LastRtcmWrittenAgeMs),
|
|
("rtcm.drop_stale", metrics.RtcmDroppedStale),
|
|
("rtcm.write_errors", metrics.RtcmWriteErrors)
|
|
);
|
|
|
|
CorrecaoRTK_Ntrip = RedisService.GetField<bool>(RedisService.ModKey(T_Code.Gps), "correcao_ntrip", false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RecordError("[GPSService.AtualizaDadosRedis] " + ex.Message);
|
|
|
|
try
|
|
{
|
|
Variaveis.OperacaoEmAndamento.Sensoriamento.InserirLog(
|
|
T_Code.Gps,
|
|
StatusModulo.Falha,
|
|
0,
|
|
"Erro ao salvar dados do GPS no Redis: " + ex.Message
|
|
);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
public static void AtualizarAtrasoPosicoes(int errosConsiderar = 0)
|
|
{
|
|
lock (_stateLock)
|
|
{
|
|
historicoPosicao.Enqueue(UltimaLeitura.Clone());
|
|
|
|
while (historicoPosicao.Count > 1 &&
|
|
(
|
|
historicoPosicao.Count >
|
|
errosConsiderar ||
|
|
errosConsiderar == 0
|
|
))
|
|
{
|
|
historicoPosicao.Dequeue();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// LOG EM WORKER
|
|
// ============================================================
|
|
|
|
private static void EnfileirarLogSnapshot()
|
|
{
|
|
try
|
|
{
|
|
string json;
|
|
|
|
lock (_stateLock)
|
|
{
|
|
GPSModel obj = UltimaLeitura.Clone();
|
|
obj.Momento = DateTime.Now;
|
|
json = JsonConvert.SerializeObject(obj);
|
|
}
|
|
|
|
_gpsLogQueue.Enqueue(json);
|
|
|
|
try { _logSignal.Release(); }
|
|
catch (SemaphoreFullException) { }
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.EnfileirarLogSnapshot] " +
|
|
ex.Message
|
|
);
|
|
}
|
|
}
|
|
|
|
private static async Task LogWriterLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
var lote = new List<string>(300);
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await _logSignal.WaitAsync(
|
|
cancellationToken
|
|
).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
|
|
string item;
|
|
|
|
while (lote.Count < 300 &&
|
|
_gpsLogQueue.TryDequeue(
|
|
out item))
|
|
{
|
|
lote.Add(item);
|
|
}
|
|
|
|
if (lote.Count < 300)
|
|
continue;
|
|
|
|
try
|
|
{
|
|
/*
|
|
* RegistrarLogDispositivo limpa a lista recebida.
|
|
* Passamos uma lista exclusiva do worker.
|
|
*/
|
|
VariaveisOperacao
|
|
.RegistrarLogDispositivo(
|
|
lote,
|
|
T_Code.Gps,
|
|
".json",
|
|
300
|
|
);
|
|
|
|
lote = new List<string>(300);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.LogWriter] " +
|
|
ex.Message
|
|
);
|
|
}
|
|
}
|
|
|
|
while (_gpsLogQueue.TryDequeue(out var pendente))
|
|
lote.Add(pendente);
|
|
|
|
if (lote.Count > 0)
|
|
{
|
|
try
|
|
{
|
|
VariaveisOperacao
|
|
.RegistrarLogDispositivo(
|
|
lote,
|
|
T_Code.Gps,
|
|
".json",
|
|
1
|
|
);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// WORKERS E SHUTDOWN
|
|
// ============================================================
|
|
|
|
private static void EnsureBackgroundWorkersStarted()
|
|
{
|
|
lock (_workerLock)
|
|
{
|
|
if (_serviceCts != null &&
|
|
!_serviceCts.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_serviceCts =
|
|
new CancellationTokenSource();
|
|
|
|
CancellationToken token =
|
|
_serviceCts.Token;
|
|
|
|
_nmeaWorkerTask = Task.Run(
|
|
() => NmeaWorkerLoopAsync(token),
|
|
token
|
|
);
|
|
|
|
_rtcmWriterTask = Task.Run(
|
|
() => RtcmWriterLoopAsync(token),
|
|
token
|
|
);
|
|
|
|
_logWriterTask = Task.Run(
|
|
() => LogWriterLoopAsync(token),
|
|
token
|
|
);
|
|
|
|
_localPublisherTask = Task.Run(
|
|
() => LocalPublisherLoopAsync(token),
|
|
token
|
|
);
|
|
}
|
|
}
|
|
|
|
public static async Task EncerrarAsync()
|
|
{
|
|
await PararNtripAsync().ConfigureAwait(false);
|
|
|
|
CancellationTokenSource cts;
|
|
Task[] tasks;
|
|
|
|
lock (_workerLock)
|
|
{
|
|
cts = _serviceCts;
|
|
|
|
tasks = new[]
|
|
{
|
|
_nmeaWorkerTask,
|
|
_rtcmWriterTask,
|
|
_logWriterTask,
|
|
_localPublisherTask
|
|
}
|
|
.Where(x => x != null)
|
|
.ToArray();
|
|
|
|
_serviceCts = null;
|
|
_nmeaWorkerTask = null;
|
|
_rtcmWriterTask = null;
|
|
_logWriterTask = null;
|
|
_localPublisherTask = null;
|
|
}
|
|
|
|
if (cts != null)
|
|
{
|
|
try { cts.Cancel(); } catch { }
|
|
}
|
|
|
|
try
|
|
{
|
|
await Task.WhenAll(tasks)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception ex)
|
|
{
|
|
RecordError(
|
|
"[GPSService.EncerrarAsync] " +
|
|
ex.Message
|
|
);
|
|
}
|
|
|
|
if (cts != null)
|
|
cts.Dispose();
|
|
|
|
CancellationTokenSource configCts =
|
|
Interlocked.Exchange(
|
|
ref _configurationCts,
|
|
null
|
|
);
|
|
|
|
if (configCts != null)
|
|
{
|
|
try { configCts.Cancel(); } catch { }
|
|
}
|
|
|
|
/*
|
|
* Aguarda uma configuração em andamento abandonar os locks.
|
|
* O token acima interrompe os delays cooperativos.
|
|
*/
|
|
await _configurationLock.WaitAsync()
|
|
.ConfigureAwait(false);
|
|
_configurationLock.Release();
|
|
|
|
await _serialWriteLock.WaitAsync()
|
|
.ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
lock (_portLifecycleLock)
|
|
{
|
|
SerialPort porta = PortaGPS;
|
|
PortaGPS = null;
|
|
|
|
if (porta != null)
|
|
{
|
|
try
|
|
{
|
|
porta.DataReceived -=
|
|
PortaGPS_DataReceived;
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
if (porta.IsOpen)
|
|
porta.Close();
|
|
}
|
|
catch { }
|
|
|
|
try { porta.Dispose(); }
|
|
catch { }
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_serialWriteLock.Release();
|
|
}
|
|
|
|
if (configCts != null)
|
|
configCts.Dispose();
|
|
}
|
|
|
|
// ============================================================
|
|
// MÉTRICAS
|
|
// ============================================================
|
|
|
|
public static GpsTransportMetrics GetTransportMetrics()
|
|
{
|
|
int queueDepth;
|
|
int nmeaQueueDepth;
|
|
|
|
lock (_rtcmQueueLock)
|
|
queueDepth = _rtcmQueue.Count;
|
|
|
|
lock (_nmeaQueueLock)
|
|
nmeaQueueDepth = _nmeaQueue.Count;
|
|
|
|
string lastError;
|
|
|
|
lock (_metricsTextLock)
|
|
lastError = _lastError;
|
|
|
|
return new GpsTransportMetrics
|
|
{
|
|
SerialConnected = Iniciado,
|
|
SerialPortName =
|
|
PortaGPS != null
|
|
? PortaGPS.PortName
|
|
: null,
|
|
|
|
LastSerialRxAgeMs =
|
|
GetAgeMs(
|
|
Interlocked.Read(
|
|
ref _lastSerialRxMono
|
|
)
|
|
),
|
|
|
|
LastValidNmeaAgeMs =
|
|
GetAgeMs(
|
|
Interlocked.Read(
|
|
ref _lastValidNmeaMono
|
|
)
|
|
),
|
|
|
|
LastValidGgaAgeMs =
|
|
GetAgeMs(
|
|
Interlocked.Read(
|
|
ref _lastValidGgaMono
|
|
)
|
|
),
|
|
|
|
LastValidThsAgeMs =
|
|
GetAgeMs(
|
|
Interlocked.Read(
|
|
ref _lastValidThsMono
|
|
)
|
|
),
|
|
|
|
SerialBytesReceived =
|
|
Interlocked.Read(
|
|
ref _serialBytesReceived
|
|
),
|
|
|
|
NmeaValid =
|
|
Interlocked.Read(ref _nmeaValid),
|
|
|
|
NmeaInvalid =
|
|
Interlocked.Read(ref _nmeaInvalid),
|
|
|
|
NmeaChecksumErrors =
|
|
Interlocked.Read(
|
|
ref _nmeaChecksumErrors
|
|
),
|
|
|
|
NmeaBufferResets =
|
|
Interlocked.Read(
|
|
ref _nmeaBufferResets
|
|
),
|
|
|
|
NmeaQueued =
|
|
Interlocked.Read(ref _nmeaQueued),
|
|
|
|
NmeaDroppedQueue =
|
|
Interlocked.Read(
|
|
ref _nmeaDroppedQueue
|
|
),
|
|
|
|
NmeaQueueDepth = nmeaQueueDepth,
|
|
|
|
RtcmReceived =
|
|
Interlocked.Read(ref _rtcmReceived),
|
|
|
|
RtcmQueued =
|
|
Interlocked.Read(ref _rtcmQueued),
|
|
|
|
RtcmWritten =
|
|
Interlocked.Read(ref _rtcmWritten),
|
|
|
|
RtcmDroppedQueue =
|
|
Interlocked.Read(
|
|
ref _rtcmDroppedQueue
|
|
),
|
|
|
|
RtcmDroppedStale =
|
|
Interlocked.Read(
|
|
ref _rtcmDroppedStale
|
|
),
|
|
|
|
RtcmDroppedInvalid =
|
|
Interlocked.Read(
|
|
ref _rtcmDroppedInvalid
|
|
),
|
|
|
|
RtcmWriteErrors =
|
|
Interlocked.Read(
|
|
ref _rtcmWriteErrors
|
|
),
|
|
|
|
RtcmQueueDepth = queueDepth,
|
|
|
|
LastRtcmReceivedAgeMs =
|
|
GetAgeMs(
|
|
Interlocked.Read(
|
|
ref _lastRtcmReceivedMono
|
|
)
|
|
),
|
|
|
|
LastRtcmWrittenAgeMs =
|
|
GetAgeMs(
|
|
Interlocked.Read(
|
|
ref _lastRtcmWrittenMono
|
|
)
|
|
),
|
|
|
|
ConfigurationRuns =
|
|
Interlocked.Read(
|
|
ref _configurationRuns
|
|
),
|
|
|
|
ConfigurationErrors =
|
|
Interlocked.Read(
|
|
ref _configurationErrors
|
|
),
|
|
|
|
NtripRunning = LoopRTK_Ntrip,
|
|
|
|
NtripReconnects =
|
|
Interlocked.Read(
|
|
ref _ntripReconnects
|
|
),
|
|
|
|
NtripBytesReceived =
|
|
Interlocked.Read(
|
|
ref _ntripBytesReceived
|
|
),
|
|
|
|
NtripErrors =
|
|
Interlocked.Read(
|
|
ref _ntripErrors
|
|
),
|
|
|
|
LastError = lastError
|
|
};
|
|
}
|
|
|
|
private static void RecordError(string error)
|
|
{
|
|
lock (_metricsTextLock)
|
|
_lastError = error;
|
|
|
|
Variaveis.MostrarLog(error);
|
|
}
|
|
|
|
// ============================================================
|
|
// HELPERS
|
|
// ============================================================
|
|
|
|
private static void WriteSerialUnsafe(byte[] bytes, int count)
|
|
{
|
|
SerialPort porta = PortaGPS;
|
|
|
|
if (porta == null || !porta.IsOpen)
|
|
throw new IOException(
|
|
"Porta GPS não está aberta."
|
|
);
|
|
|
|
porta.Write(bytes, 0, count);
|
|
}
|
|
|
|
private static double GetAgeMs(long timestamp)
|
|
{
|
|
if (timestamp <= 0)
|
|
return double.PositiveInfinity;
|
|
|
|
long delta =
|
|
Stopwatch.GetTimestamp() - timestamp;
|
|
|
|
if (delta <= 0)
|
|
return 0;
|
|
|
|
return delta * 1000.0 /
|
|
Stopwatch.Frequency;
|
|
}
|
|
|
|
private static string GetField(string[] fields, int index, string fallback = "")
|
|
{
|
|
return fields != null &&
|
|
index >= 0 &&
|
|
index < fields.Length
|
|
? fields[index] ?? fallback
|
|
: fallback;
|
|
}
|
|
|
|
private static string StripChecksum(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
return string.Empty;
|
|
|
|
int index = value.IndexOf('*');
|
|
|
|
return index >= 0
|
|
? value.Substring(0, index)
|
|
: value;
|
|
}
|
|
|
|
private static bool TryParseDmm(string raw, string hemisphere, int degreeDigits, out double result)
|
|
{
|
|
result = 0;
|
|
|
|
if (string.IsNullOrWhiteSpace(raw) || raw.Length <= degreeDigits)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
double degrees;
|
|
double minutes;
|
|
|
|
if (!double.TryParse(
|
|
raw.Substring(0, degreeDigits),
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out degrees) ||
|
|
!double.TryParse(
|
|
raw.Substring(degreeDigits),
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out minutes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
result = degrees + minutes / 60.0;
|
|
|
|
if (string.Equals(
|
|
hemisphere,
|
|
"S",
|
|
StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(
|
|
hemisphere,
|
|
"W",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
result = -result;
|
|
}
|
|
|
|
return !double.IsNaN(result) &&
|
|
!double.IsInfinity(result);
|
|
}
|
|
|
|
private static bool TryParseNmeaTime(string raw, out DateTime result)
|
|
{
|
|
result = default(DateTime);
|
|
|
|
if (string.IsNullOrWhiteSpace(raw) || raw.Length < 6)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int hh;
|
|
int mm;
|
|
double ss;
|
|
|
|
if (!int.TryParse(
|
|
raw.Substring(0, 2),
|
|
out hh) ||
|
|
!int.TryParse(
|
|
raw.Substring(2, 2),
|
|
out mm) ||
|
|
!double.TryParse(
|
|
raw.Substring(4),
|
|
NumberStyles.Float,
|
|
CultureInfo.InvariantCulture,
|
|
out ss))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int seconds = (int)Math.Floor(ss);
|
|
int milliseconds = (int)Math.Round(
|
|
(ss - seconds) * 1000.0
|
|
);
|
|
|
|
if (milliseconds >= 1000)
|
|
{
|
|
seconds++;
|
|
milliseconds = 0;
|
|
}
|
|
|
|
try
|
|
{
|
|
result = DateTime.UtcNow.Date
|
|
.AddHours(hh)
|
|
.AddMinutes(mm)
|
|
.AddSeconds(seconds)
|
|
.AddMilliseconds(milliseconds)
|
|
.ToLocalTime();
|
|
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryParseRmcDateTime(string dateRaw, string timeRaw, out DateTime result)
|
|
{
|
|
result = default(DateTime);
|
|
|
|
if (string.IsNullOrWhiteSpace(dateRaw) || dateRaw.Length != 6 || string.IsNullOrWhiteSpace(timeRaw) || timeRaw.Length < 6)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int day;
|
|
int month;
|
|
int yearShort;
|
|
int hour;
|
|
int minute;
|
|
int second;
|
|
|
|
if (!int.TryParse(
|
|
dateRaw.Substring(0, 2),
|
|
out day) ||
|
|
!int.TryParse(
|
|
dateRaw.Substring(2, 2),
|
|
out month) ||
|
|
!int.TryParse(
|
|
dateRaw.Substring(4, 2),
|
|
out yearShort) ||
|
|
!int.TryParse(
|
|
timeRaw.Substring(0, 2),
|
|
out hour) ||
|
|
!int.TryParse(
|
|
timeRaw.Substring(2, 2),
|
|
out minute) ||
|
|
!int.TryParse(
|
|
timeRaw.Substring(4, 2),
|
|
out second))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
result = new DateTime(
|
|
2000 + yearShort,
|
|
month,
|
|
day,
|
|
hour,
|
|
minute,
|
|
second,
|
|
DateTimeKind.Utc
|
|
);
|
|
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private sealed class RtcmEnvelope
|
|
{
|
|
public byte[] Bytes { get; set; }
|
|
public long ReceivedMonotonic { get; set; }
|
|
public DateTime ReceivedUtc { get; set; }
|
|
public long Sequence { get; set; }
|
|
public int MessageType { get; set; }
|
|
public RtcmSource Source { get; set; }
|
|
}
|
|
|
|
private enum RtcmSource
|
|
{
|
|
Mqtt,
|
|
Ntrip
|
|
}
|
|
|
|
private sealed class NtripHeaderResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string StatusLine { get; set; }
|
|
public byte[] RemainingBytes { get; set; }
|
|
}
|
|
}
|
|
|
|
public sealed class GpsTransportMetrics
|
|
{
|
|
public bool SerialConnected { get; set; }
|
|
public string SerialPortName { get; set; }
|
|
|
|
public double LastSerialRxAgeMs { get; set; }
|
|
public double LastValidNmeaAgeMs { get; set; }
|
|
public double LastValidGgaAgeMs { get; set; }
|
|
public double LastValidThsAgeMs { get; set; }
|
|
|
|
public long SerialBytesReceived { get; set; }
|
|
|
|
public long NmeaValid { get; set; }
|
|
public long NmeaInvalid { get; set; }
|
|
public long NmeaChecksumErrors { get; set; }
|
|
public long NmeaBufferResets { get; set; }
|
|
public long NmeaQueued { get; set; }
|
|
public long NmeaDroppedQueue { get; set; }
|
|
public int NmeaQueueDepth { get; set; }
|
|
|
|
public long RtcmReceived { get; set; }
|
|
public long RtcmQueued { get; set; }
|
|
public long RtcmWritten { get; set; }
|
|
public long RtcmDroppedQueue { get; set; }
|
|
public long RtcmDroppedStale { get; set; }
|
|
public long RtcmDroppedInvalid { get; set; }
|
|
public long RtcmWriteErrors { get; set; }
|
|
public int RtcmQueueDepth { get; set; }
|
|
public double LastRtcmReceivedAgeMs { get; set; }
|
|
public double LastRtcmWrittenAgeMs { get; set; }
|
|
|
|
public long ConfigurationRuns { get; set; }
|
|
public long ConfigurationErrors { get; set; }
|
|
|
|
public bool NtripRunning { get; set; }
|
|
public long NtripReconnects { get; set; }
|
|
public long NtripBytesReceived { get; set; }
|
|
public long NtripErrors { get; set; }
|
|
|
|
public string LastError { get; set; }
|
|
}
|
|
|
|
public class GgaFix
|
|
{
|
|
public DateTime TsUtc { get; private set; }
|
|
public double LatDeg { get; private set; }
|
|
public double LonDeg { get; private set; }
|
|
public double AltElipsoidalM { get; private set; }
|
|
public double HeadingDeg { get; private set; }
|
|
public TiposCorrecaoGPS FixQuality { get; private set; }
|
|
|
|
public GgaFix(
|
|
DateTime tsUtc,
|
|
double latDeg,
|
|
double lonDeg,
|
|
double altElipsoidalM,
|
|
double headingDeg,
|
|
TiposCorrecaoGPS fixQuality)
|
|
{
|
|
TsUtc = tsUtc;
|
|
LatDeg = latDeg;
|
|
LonDeg = lonDeg;
|
|
AltElipsoidalM = altElipsoidalM;
|
|
HeadingDeg = headingDeg;
|
|
FixQuality = fixQuality;
|
|
}
|
|
}
|
|
|
|
public class GeoLeverArm
|
|
{
|
|
private readonly double xFisico;
|
|
private readonly double yFisico;
|
|
private readonly double xCampo;
|
|
private readonly double yCampo;
|
|
|
|
private readonly bool invertHeading;
|
|
private readonly bool invertFrontal;
|
|
private readonly bool mirrorLateral;
|
|
|
|
public GeoLeverArm(
|
|
double offsetFisicoFrontalCm = 0,
|
|
double offsetFisicoLateralCm = 0,
|
|
double offsetCampoFrontalCm = 0,
|
|
double offsetCampoLateralCm = 0,
|
|
bool invH = false,
|
|
bool invF = false,
|
|
bool mirrL = false
|
|
)
|
|
{
|
|
xFisico = offsetFisicoFrontalCm / 100.0;
|
|
yFisico = offsetFisicoLateralCm / 100.0;
|
|
xCampo = offsetCampoFrontalCm / 100.0;
|
|
yCampo = offsetCampoLateralCm / 100.0;
|
|
|
|
invertHeading = invH;
|
|
invertFrontal = invF;
|
|
mirrorLateral = mirrL;
|
|
}
|
|
|
|
private static double ToRad(double deg)
|
|
{
|
|
return deg * Math.PI / 180.0;
|
|
}
|
|
|
|
public double FrontalTotalCm
|
|
{
|
|
get { return (xFisico + xCampo) * 100.0; }
|
|
}
|
|
|
|
public double LateralTotalCm
|
|
{
|
|
get { return (yFisico + yCampo) * 100.0; }
|
|
}
|
|
|
|
public double FrontalCampoCm
|
|
{
|
|
get { return (xCampo) * 100.0; }
|
|
}
|
|
|
|
public double LateralCampoCm
|
|
{
|
|
get { return (yCampo) * 100.0; }
|
|
}
|
|
|
|
public (double lat, double lon) FixLeverArmLatLon_Fast(double latAnt_deg, double lonAnt_deg, double headingDeg, double headingFreq)
|
|
{
|
|
if (headingFreq < 1)
|
|
return (latAnt_deg, lonAnt_deg);
|
|
|
|
double xTotal = xFisico + xCampo;
|
|
double yTotal = yFisico + yCampo;
|
|
|
|
if (invertFrontal)
|
|
xTotal = -xTotal;
|
|
|
|
if (mirrorLateral)
|
|
yTotal = -yTotal;
|
|
|
|
double th = ToRad(headingDeg);
|
|
|
|
if (invertHeading)
|
|
th = -th;
|
|
|
|
double fE = Math.Sin(th);
|
|
double fN = Math.Cos(th);
|
|
|
|
double rE = fN;
|
|
double rN = -fE;
|
|
|
|
double dE =
|
|
xTotal * fE +
|
|
yTotal * rE;
|
|
|
|
double dN =
|
|
xTotal * fN +
|
|
yTotal * rN;
|
|
|
|
double latRad = ToRad(latAnt_deg);
|
|
|
|
double dLatDeg =
|
|
dN / GPSUtils.RaioDaTerra *
|
|
180.0 / Math.PI;
|
|
|
|
double cosLat = Math.Cos(latRad);
|
|
|
|
if (Math.Abs(cosLat) < 1e-12)
|
|
return (latAnt_deg, lonAnt_deg);
|
|
|
|
double dLonDeg =
|
|
dE /
|
|
(GPSUtils.RaioDaTerra * cosLat) *
|
|
180.0 / Math.PI;
|
|
|
|
return (
|
|
latAnt_deg - dLatDeg,
|
|
lonAnt_deg - dLonDeg
|
|
);
|
|
}
|
|
|
|
public (double x, double y) GeodeticToENU(double lat, double lon, double lat0, double lon0)
|
|
{
|
|
double latR = ToRad(lat);
|
|
double lonR = ToRad(lon);
|
|
double lat0R = ToRad(lat0);
|
|
double lon0R = ToRad(lon0);
|
|
|
|
double dLat = latR - lat0R;
|
|
double dLon = lonR - lon0R;
|
|
|
|
double xEast =
|
|
dLon *
|
|
Math.Cos(lat0R) *
|
|
GPSUtils.RaioDaTerra;
|
|
|
|
double yNorth =
|
|
dLat * GPSUtils.RaioDaTerra;
|
|
|
|
return (xEast, yNorth);
|
|
}
|
|
}
|
|
}
|