agrobot_base/AgroBase/AgroBase/Services/GPSService.cs

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);
}
}
}