2385 lines
90 KiB
C#
2385 lines
90 KiB
C#
using AgroBase.Models.Modules;
|
|
using AgroBase.Services;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing.Imaging;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using static AgroBase.Models.Enums;
|
|
using System.Drawing.Drawing2D;
|
|
using Microsoft.Win32;
|
|
using System.Reflection;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using AgroBase.Models.Components;
|
|
using Microsoft.Web.WebView2.WinForms;
|
|
using Microsoft.Web.WebView2.Core;
|
|
using AgroBase.Services.Operadores;
|
|
using System.Diagnostics;
|
|
using Newtonsoft.Json;
|
|
using AgroMonitor;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using AgroBase.Forms.IHM.Controls.Operacao;
|
|
|
|
namespace AgroBase.Models
|
|
{
|
|
public class Variaveis
|
|
{
|
|
public static readonly bool IniciarWorkers = true;
|
|
public static readonly bool UsarIHM = true;
|
|
public static readonly bool Producao = true;
|
|
public static readonly int ExitShutdownCode = 100;
|
|
public static bool DebugMode { get; set; } = false;
|
|
public static bool Fechando { get; set; } = false;
|
|
|
|
public static string CaminhoSistema { get; set; } = AppDomain.CurrentDomain.BaseDirectory;
|
|
public static string NomeAplicacao
|
|
{
|
|
get
|
|
{
|
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
|
string name = assembly.GetName().Name;
|
|
return name.ToString();
|
|
}
|
|
}
|
|
public static string Versao
|
|
{
|
|
get
|
|
{
|
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
|
Version version = assembly.GetName().Version;
|
|
return version.ToString();
|
|
}
|
|
}
|
|
public static string CaminhoLogsDispositivos { get; } = ResolverCaminhoLocal("Logs/");
|
|
public static string CaminhoOperacoes { get; } = ResolverCaminhoLocal("Operacoes/");
|
|
public static string CaminhoOperacoesSalvas { get; } = ResolverCaminhoLocal("OperacoesSalvas/");
|
|
public static string CaminhoMapasConvertidos { get; } = ResolverCaminhoLocal("Mapas/");
|
|
public static string CaminhoParametros { get; } = ResolverCaminhoLocal("Parametros/");
|
|
public static string CaminhoModelos { get; } = ResolverCaminhoLocal(@"C:\AgroBaseModels");
|
|
private static string ResolverCaminhoLocal(string caminho)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(caminho))
|
|
throw new ArgumentException("Caminho local não informado.");
|
|
|
|
if (Path.IsPathRooted(caminho))
|
|
return Path.GetFullPath(caminho);
|
|
|
|
return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, caminho));
|
|
}
|
|
public static List<IDispositivosService> DispositivosConectados { get; set; } = new List<IDispositivosService>();
|
|
public static MqttService MqttServiceLocal { get; private set; }
|
|
public static MqttService MqttServiceBaseCritical { get; private set; }
|
|
public static MqttService MqttServiceBaseTelemetry { get; private set; }
|
|
|
|
private static readonly object _opLock = new object();
|
|
private static OperacaoModel _op;
|
|
public static OperacaoModel OperacaoEmAndamento
|
|
{
|
|
get
|
|
{
|
|
lock (_opLock)
|
|
return _op;
|
|
}
|
|
set
|
|
{
|
|
lock (_opLock)
|
|
{
|
|
if (ReferenceEquals(_op, value))
|
|
return;
|
|
|
|
_op?.PararTimersSistema();
|
|
|
|
_op = value;
|
|
|
|
_op?.IniciarTimersSistema();
|
|
|
|
RedisService.AtualizarCampos(CtxKey.DadosOperacao, ("configurado", false));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compatibilidade temporária com o restante do projeto.
|
|
/// Publicações antigas feitas por MqttServiceBase seguem pelo canal
|
|
/// de telemetria, nunca pelo canal crítico de RTCM/comandos.
|
|
/// </summary>
|
|
public static MqttService MqttServiceBase
|
|
{
|
|
get { return MqttServiceBaseTelemetry; }
|
|
set { MqttServiceBaseTelemetry = value; }
|
|
}
|
|
|
|
public static BaseLinkState BaseLink { get; } = new BaseLinkState();
|
|
|
|
public static MqttService.MqttTopicosModel TopicoLocalCoordenadasGps { get; private set; }
|
|
public static MqttService.MqttTopicosModel TopicoLocalTrajetoriaDinamica { get; private set; }
|
|
public static MqttService.MqttTopicosModel TopicoLocalSelecaoRuas { get; private set; }
|
|
|
|
public static MqttService.MqttTopicosModel TopicoBaseDiscovery { get; private set; }
|
|
public static MqttService.MqttTopicosModel TopicoBaseTelemetria { get; private set; }
|
|
public static MqttService.MqttTopicosModel TopicoBaseParametros { get; private set; }
|
|
|
|
public static MqttService.MqttTopicosModel TopicoBaseHeartbeat { get; private set; }
|
|
public static MqttService.MqttTopicosModel TopicoBaseComandos { get; private set; }
|
|
public static MqttService.MqttTopicosModel TopicoBaseRtcm { get; private set; }
|
|
public static MqttService.MqttTopicosModel TopicoBasePosicao { get; private set; }
|
|
|
|
private static readonly SemaphoreSlim _mqttLifecycleLock = new SemaphoreSlim(1, 1);
|
|
private static AsyncTaskTimerModel _tmrMqttLinkMonitor;
|
|
private static int _mqttCriticalConnectedState = -1;
|
|
//public static LoRaEspService LoraService
|
|
//{
|
|
// get
|
|
// {
|
|
// return OperacaoEmAndamento.DispSen?.Dados?.loRaService;
|
|
// }
|
|
//}
|
|
//public static LoRaParametrosModel LoraBaseParametros
|
|
//{
|
|
// get
|
|
// {
|
|
// return OperacaoEmAndamento.DispSen?.Dados?.LoRaParametrosBase;
|
|
// }
|
|
//}
|
|
public static bool IsAgroBase
|
|
{
|
|
get
|
|
{
|
|
return CaminhoSistema.Contains("AgroBase") && !CaminhoSistema.Contains("OperationControl");
|
|
}
|
|
}
|
|
|
|
public static byte ID_Num_sMOD { get; } = 0;
|
|
public static byte ID_Num_sTOD { get; } = 250;
|
|
public static byte ID_Num_sLRA { get; } = 251;
|
|
|
|
/// <summary>
|
|
/// Nome antigo preservado para compatibilidade. Novos pontos de startup
|
|
/// devem aguardar IniciarMqttAsync diretamente.
|
|
/// </summary>
|
|
public static Task IniciarMQTT()
|
|
{
|
|
return IniciarMqttAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inicializa deterministicamente os clientes MQTT e seus tópicos.
|
|
/// Uma segunda chamada encerra por completo a geração anterior antes
|
|
/// de criar novos clientes.
|
|
/// </summary>
|
|
public static async Task IniciarMqttAsync(CancellationToken cancellationToken = default(CancellationToken))
|
|
{
|
|
await _mqttLifecycleLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
if (Fechando)
|
|
return;
|
|
|
|
string roverId = VariaveisEquipamento.Parametros.serial_number;
|
|
string baseIp = VariaveisEquipamento.Parametros.base_ip;
|
|
|
|
if (string.IsNullOrWhiteSpace(roverId))
|
|
throw new InvalidOperationException("O serial_number do rover não foi configurado.");
|
|
|
|
if (string.IsNullOrWhiteSpace(baseIp))
|
|
throw new InvalidOperationException("O base_ip do rover não foi configurado.");
|
|
|
|
await EncerrarMqttInternoAsync("Reinicialização MQTT").ConfigureAwait(false);
|
|
|
|
BaseLink.BeginNewSession(roverId);
|
|
Interlocked.Exchange(ref _mqttCriticalConnectedState, -1);
|
|
|
|
MqttServiceLocal = new MqttService(
|
|
"localhost",
|
|
1883,
|
|
roverId,
|
|
true,
|
|
msg => Console.WriteLine($"[MQTT LOCAL localhost:1883] - {msg}"));
|
|
|
|
MqttServiceBaseCritical = new MqttService(
|
|
baseIp,
|
|
1883,
|
|
roverId + "-critical",
|
|
false,
|
|
msg => Console.WriteLine($"[MQTT CRITICAL {baseIp}:1883] - {msg}"));
|
|
|
|
MqttServiceBaseTelemetry = new MqttService(
|
|
baseIp,
|
|
1883,
|
|
roverId + "-telemetry",
|
|
false,
|
|
msg => Console.WriteLine($"[MQTT TELEMETRY {baseIp}:1883] - {msg}"));
|
|
|
|
await ConfigurarTopicosMqttCriticosAsync(roverId).ConfigureAwait(false);
|
|
await ConfigurarTopicosMqttTelemetriaAsync(roverId).ConfigureAwait(false);
|
|
|
|
_tmrMqttLinkMonitor = new AsyncTaskTimerModel(
|
|
"tmrMqttLinkMonitor",
|
|
MonitorarLinkMqttAsync,
|
|
interval: 500,
|
|
timeout: 2000,
|
|
scheduleMode: AsyncTaskTimerScheduleMode.FixedRateSkipMissed,
|
|
runImmediately: true);
|
|
|
|
_tmrMqttLinkMonitor.DebugMessages = false;
|
|
_tmrMqttLinkMonitor.Start();
|
|
|
|
await Task.WhenAll(
|
|
MqttServiceLocal.StartAsync(),
|
|
MqttServiceBaseCritical.StartAsync(),
|
|
MqttServiceBaseTelemetry.StartAsync()
|
|
).ConfigureAwait(false);
|
|
|
|
AtualizarEstadoBrokerCritico();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BaseLink.RecordError("Falha ao inicializar MQTT: " + ex.Message);
|
|
Console.WriteLine("[MQTT] Falha durante inicialização: " + ex);
|
|
|
|
await EncerrarMqttInternoAsync(
|
|
"Falha durante inicialização MQTT"
|
|
).ConfigureAwait(false);
|
|
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_mqttLifecycleLock.Release();
|
|
}
|
|
}
|
|
|
|
public static async Task EncerrarMqttAsync()
|
|
{
|
|
await _mqttLifecycleLock.WaitAsync().ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
await EncerrarMqttInternoAsync("Encerramento solicitado").ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
_mqttLifecycleLock.Release();
|
|
}
|
|
}
|
|
|
|
private static async Task ConfigurarTopicosMqttCriticosAsync(string roverId)
|
|
{
|
|
TopicoBaseHeartbeat = await MqttServiceBaseCritical.AdicionarNovoTopico(
|
|
VariaveisEquipamento.TopicoMqttHeartbeat.Replace("<id>", roverId),
|
|
inscrever: true,
|
|
mensagensManter: 1,
|
|
callback: message =>
|
|
{
|
|
MarcarBrokerCriticoConectado();
|
|
BaseLink.MarkHeartbeat();
|
|
|
|
// Compatibilidade temporária com tmrComunicacao_Tick.
|
|
// Será removido quando discovery/telemetria migrarem para
|
|
// serviços próprios baseados em BaseLinkState.
|
|
VariaveisOperacao.MarcarHeartbeatBaseLegado(DateTime.Now);
|
|
return Task.CompletedTask;
|
|
},
|
|
payloadType: MqttPayloadType.Text,
|
|
dispatchMode: MqttDispatchMode.Inline,
|
|
queueCapacity: 1,
|
|
overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
|
|
qos: MqttQosLevel.AtMostOnce,
|
|
retain: false
|
|
).ConfigureAwait(false);
|
|
|
|
TopicoBaseComandos = await MqttServiceBaseCritical.AdicionarNovoTopico(
|
|
VariaveisEquipamento.TopicoMqttComandos.Replace("<id>", roverId),
|
|
inscrever: true,
|
|
mensagensManter: 2,
|
|
callback: message =>
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message.Mensagem))
|
|
return Task.CompletedTask;
|
|
|
|
try
|
|
{
|
|
OperacaoComandoBaseModel cmd =
|
|
JsonConvert.DeserializeObject<OperacaoComandoBaseModel>(message.Mensagem);
|
|
|
|
if (cmd == null)
|
|
return Task.CompletedTask;
|
|
|
|
MarcarBrokerCriticoConectado();
|
|
BaseLink.MarkCommand();
|
|
OperacaoEmAndamento?.ExecutaComandoDaBase(cmd);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BaseLink.RecordError("Erro no comando MQTT: " + ex.Message);
|
|
Console.WriteLine("Erro ao deserializar comando da base: " + ex.Message);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
},
|
|
payloadType: MqttPayloadType.Text,
|
|
dispatchMode: MqttDispatchMode.Sequential,
|
|
queueCapacity: 64,
|
|
overflowPolicy: MqttQueueOverflowPolicy.DropNewest,
|
|
qos: MqttQosLevel.AtLeastOnce,
|
|
retain: false
|
|
).ConfigureAwait(false);
|
|
|
|
TopicoBaseRtcm = await MqttServiceBaseCritical.AdicionarNovoTopico(
|
|
VariaveisMonitoramento.TopicoMqttRTCM,
|
|
inscrever: true,
|
|
mensagensManter: 1,
|
|
callback: message =>
|
|
{
|
|
byte[] bytes = message.Bytes;
|
|
|
|
if (bytes == null || bytes.Length == 0)
|
|
return Task.CompletedTask;
|
|
|
|
try
|
|
{
|
|
MarcarBrokerCriticoConectado();
|
|
BaseLink.MarkRtcm();
|
|
GPSService.AplicarCorrecaoRTK_Mqtt(bytes, bytes.Length);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BaseLink.RecordError("Erro ao aplicar RTCM MQTT: " + ex.Message);
|
|
Console.WriteLine("Erro ao aplicar RTCM da base: " + ex.Message);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
},
|
|
payloadType: MqttPayloadType.Binary,
|
|
dispatchMode: MqttDispatchMode.Sequential,
|
|
queueCapacity: 32,
|
|
overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
|
|
qos: MqttQosLevel.AtMostOnce,
|
|
retain: false
|
|
).ConfigureAwait(false);
|
|
|
|
TopicoBasePosicao = await MqttServiceBaseCritical.AdicionarNovoTopico(
|
|
VariaveisMonitoramento.TopicoMqttPosicao,
|
|
inscrever: true,
|
|
mensagensManter: 1,
|
|
callback: message =>
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message.Mensagem))
|
|
return Task.CompletedTask;
|
|
|
|
try
|
|
{
|
|
GPSModel posicao = JsonConvert.DeserializeObject<GPSModel>(message.Mensagem);
|
|
|
|
if (posicao == null)
|
|
return Task.CompletedTask;
|
|
|
|
posicao.Momento = DateTime.Now;
|
|
VariaveisOperacao.AtualizarPosicaoBase(posicao);
|
|
|
|
MarcarBrokerCriticoConectado();
|
|
BaseLink.MarkPosition();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BaseLink.RecordError("Erro na posição MQTT da base: " + ex.Message);
|
|
Console.WriteLine("Erro ao deserializar dados da base: " + ex.Message);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
},
|
|
payloadType: MqttPayloadType.Text,
|
|
dispatchMode: MqttDispatchMode.LatestOnly,
|
|
queueCapacity: 1,
|
|
overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
|
|
qos: MqttQosLevel.AtMostOnce,
|
|
retain: false
|
|
).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task ConfigurarTopicosMqttTelemetriaAsync(string roverId)
|
|
{
|
|
TopicoBaseDiscovery = await MqttServiceBaseTelemetry.AdicionarNovoTopico(
|
|
VariaveisMonitoramento.TopicoMqttDispositivos,
|
|
inscrever: false,
|
|
mensagensManter: 0,
|
|
callback: null,
|
|
payloadType: MqttPayloadType.Text,
|
|
dispatchMode: MqttDispatchMode.LatestOnly,
|
|
queueCapacity: 1,
|
|
overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
|
|
qos: MqttQosLevel.AtMostOnce,
|
|
retain: false
|
|
).ConfigureAwait(false);
|
|
|
|
TopicoBaseTelemetria = await MqttServiceBaseTelemetry.AdicionarNovoTopico(
|
|
VariaveisEquipamento.TopicoMqttTelemetria.Replace("<id>", roverId),
|
|
inscrever: false,
|
|
mensagensManter: 0,
|
|
callback: null,
|
|
payloadType: MqttPayloadType.Text,
|
|
dispatchMode: MqttDispatchMode.LatestOnly,
|
|
queueCapacity: 1,
|
|
overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
|
|
qos: MqttQosLevel.AtMostOnce,
|
|
retain: false
|
|
).ConfigureAwait(false);
|
|
|
|
TopicoBaseParametros = await MqttServiceBaseTelemetry.AdicionarNovoTopico(
|
|
VariaveisEquipamento.TopicoMqttParametros.Replace("<id>", roverId),
|
|
inscrever: false,
|
|
mensagensManter: 0,
|
|
callback: null,
|
|
payloadType: MqttPayloadType.Text,
|
|
dispatchMode: MqttDispatchMode.LatestOnly,
|
|
queueCapacity: 1,
|
|
overflowPolicy: MqttQueueOverflowPolicy.DropOldest,
|
|
qos: MqttQosLevel.AtLeastOnce,
|
|
retain: false
|
|
).ConfigureAwait(false);
|
|
}
|
|
|
|
private static Task MonitorarLinkMqttAsync(CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
AtualizarEstadoBrokerCritico();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private static void AtualizarEstadoBrokerCritico()
|
|
{
|
|
bool conectado = MqttServiceBaseCritical?.StatusConexao() == true;
|
|
|
|
if (conectado)
|
|
{
|
|
MarcarBrokerCriticoConectado();
|
|
return;
|
|
}
|
|
|
|
int anterior = Interlocked.Exchange(ref _mqttCriticalConnectedState, 0);
|
|
|
|
// O estado inicial desconectado não precisa ser contado como queda.
|
|
if (anterior == 1)
|
|
{
|
|
string motivo = MqttServiceBaseCritical?
|
|
.GetMetrics()?
|
|
.LastError;
|
|
|
|
BaseLink.MarkBrokerDisconnected(
|
|
string.IsNullOrWhiteSpace(motivo)
|
|
? "Cliente MQTT crítico desconectado"
|
|
: motivo
|
|
);
|
|
}
|
|
}
|
|
|
|
private static void MarcarBrokerCriticoConectado()
|
|
{
|
|
int anterior = Interlocked.Exchange(ref _mqttCriticalConnectedState, 1);
|
|
|
|
if (anterior != 1)
|
|
BaseLink.MarkBrokerConnected();
|
|
}
|
|
|
|
private static async Task EncerrarMqttInternoAsync(string motivo)
|
|
{
|
|
AsyncTaskTimerModel monitor = _tmrMqttLinkMonitor;
|
|
_tmrMqttLinkMonitor = null;
|
|
|
|
if (monitor != null)
|
|
{
|
|
try
|
|
{
|
|
await monitor.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("[MQTT] Erro ao encerrar monitor do link: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
MqttService local = MqttServiceLocal;
|
|
MqttService critical = MqttServiceBaseCritical;
|
|
MqttService telemetry = MqttServiceBaseTelemetry;
|
|
|
|
MqttServiceLocal = null;
|
|
MqttServiceBaseCritical = null;
|
|
MqttServiceBaseTelemetry = null;
|
|
|
|
LimparReferenciasTopicosMqtt();
|
|
|
|
MqttService[] services = new[] { local, critical, telemetry }
|
|
.Where(x => x != null)
|
|
.Distinct()
|
|
.ToArray();
|
|
|
|
foreach (MqttService service in services)
|
|
{
|
|
try
|
|
{
|
|
await service.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(
|
|
"[MQTT] Erro ao encerrar cliente '" +
|
|
service.ClientId + "': " + ex.Message
|
|
);
|
|
}
|
|
}
|
|
|
|
int anterior = Interlocked.Exchange(ref _mqttCriticalConnectedState, 0);
|
|
|
|
if (anterior == 1 || BaseLink.BrokerConnected)
|
|
BaseLink.MarkBrokerDisconnected(motivo);
|
|
}
|
|
|
|
private static void LimparReferenciasTopicosMqtt()
|
|
{
|
|
TopicoLocalCoordenadasGps = null;
|
|
TopicoLocalTrajetoriaDinamica = null;
|
|
TopicoLocalSelecaoRuas = null;
|
|
|
|
TopicoBaseDiscovery = null;
|
|
TopicoBaseTelemetria = null;
|
|
TopicoBaseParametros = null;
|
|
|
|
TopicoBaseHeartbeat = null;
|
|
TopicoBaseComandos = null;
|
|
TopicoBaseRtcm = null;
|
|
TopicoBasePosicao = null;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static UdpReliableChannel UdpChannel;
|
|
private static Action<byte, byte[], IPEndPoint> _onPacketHandler;
|
|
|
|
// Último pacote UDP recebido (qualquer type, inclusive heartbeat)
|
|
private static long _lastPacketTicksUtc = 0;
|
|
|
|
// Último comando válido recebido (somente type 0x01 + parse ok)
|
|
private static long _lastValidCmdTicksUtc = 0;
|
|
|
|
// Guards anti-reentrância
|
|
private static int _failSafeTickRunning = 0;
|
|
private static int _udpWatchdogTickRunning = 0;
|
|
private static int _udpRestarting = 0;
|
|
|
|
private static AsyncTaskTimerModel _tmrFailSafe;
|
|
private static AsyncTaskTimerModel _tmrUdpWatchdog;
|
|
|
|
private const int FAILSAFE_MS = 400;
|
|
private const int UDP_RESTART_MS = 5000;
|
|
|
|
|
|
public static void IniciarUDP()
|
|
{
|
|
StartUdpChannel();
|
|
|
|
_tmrFailSafe?.Dispose();
|
|
_tmrFailSafe = new AsyncTaskTimerModel("tmrFailSafe", tmrFailSafe_Tick, 100);
|
|
_tmrFailSafe.Start();
|
|
|
|
_tmrUdpWatchdog?.Dispose();
|
|
_tmrUdpWatchdog = new AsyncTaskTimerModel("tmrUdpWatchdog", tmrUdpWatchdog_Tick, 250);
|
|
_tmrUdpWatchdog.Start();
|
|
}
|
|
|
|
private static void StartUdpChannel()
|
|
{
|
|
//StopUdpChannel();
|
|
|
|
UdpChannel?.Stop();
|
|
UdpChannel?.Dispose();
|
|
UdpChannel = new UdpReliableChannel();
|
|
UdpChannel.Start(VariaveisPortas.Ethernet_UDP_RX);
|
|
|
|
// cria 1 vez e reutiliza a MESMA referência
|
|
_onPacketHandler = OnUdpPacket;
|
|
UdpChannel.OnPacket += _onPacketHandler;
|
|
|
|
Interlocked.Exchange(ref _lastPacketTicksUtc, DateTime.UtcNow.Ticks);
|
|
}
|
|
|
|
public static void StopUdpChannel()
|
|
{
|
|
try
|
|
{
|
|
_tmrFailSafe?.Dispose();
|
|
_tmrUdpWatchdog?.Dispose();
|
|
|
|
UdpChannel?.Stop();
|
|
UdpChannel?.Dispose();
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private static void OnUdpPacket(byte type, byte[] payload, IPEndPoint from)
|
|
{
|
|
// Qualquer pacote marca link vivo (inclusive heartbeat)
|
|
Interlocked.Exchange(ref _lastPacketTicksUtc, DateTime.UtcNow.Ticks);
|
|
|
|
// Heartbeat: não faz nada além de manter vivo.
|
|
if (type == UdpReliableChannel.TYPE_HEARTBEAT)
|
|
{
|
|
Console.WriteLine("[UDP] Heartbeat");
|
|
return;
|
|
}
|
|
|
|
//Console.WriteLine($"[UDP] Commnad {JsonConvert.SerializeObject(payload)}");
|
|
|
|
// Só controle entra daqui
|
|
if (type != UdpReliableChannel.TYPE_COMMAND) return;
|
|
|
|
if (!UdpCtrlMessage.TryParse(payload, out var msg)) return;
|
|
|
|
//Console.WriteLine(JsonConvert.SerializeObject(msg));
|
|
|
|
// Comando válido (controle) atualiza o failsafe
|
|
Interlocked.Exchange(ref _lastValidCmdTicksUtc, DateTime.UtcNow.Ticks);
|
|
|
|
bool emergencia = (msg.Flags & UdpCtrlFlags.Emergencia) != 0;
|
|
bool pausa = (msg.Flags & UdpCtrlFlags.Pausa) != 0;
|
|
|
|
var disp = (T_Code)msg.Device;
|
|
|
|
var controle = new OperacaoComandoBaseModel()
|
|
{
|
|
Emergencia = emergencia,
|
|
Pausa = pausa,
|
|
Tecla = msg.Key,
|
|
Solto = msg.released,
|
|
Dispositivo = disp,
|
|
Controle = new OperacaoComandoBaseControleModel(),
|
|
};
|
|
|
|
if ((msg.Flags & UdpCtrlFlags.HasPayload) != 0)
|
|
{
|
|
if (disp == T_Code.Dir)
|
|
{
|
|
controle.Controle.AnguloSP = (msg.P1 / 100.0);
|
|
controle.Controle.TipoMovimentoDirecional = (TipoMovimentoDirecional)(byte)msg.P2;
|
|
}
|
|
else if (disp == T_Code.Mov)
|
|
{
|
|
controle.Controle.PercentualVelocidadeSP = (msg.P1 / 100.0);
|
|
controle.Controle.EmFreio = msg.P2 == 1;
|
|
}
|
|
}
|
|
|
|
OperacaoEmAndamento?.ExecutaComandoDaBase(controle);
|
|
}
|
|
|
|
// ===== FAILSAFE: só considera comando de controle válido (não heartbeat) =====
|
|
public static async Task tmrFailSafe_Tick()
|
|
{
|
|
if (Interlocked.Exchange(ref _failSafeTickRunning, 1) == 1)
|
|
return;
|
|
|
|
try
|
|
{
|
|
long ticks = Interlocked.Read(ref _lastValidCmdTicksUtc);
|
|
if (ticks == 0) return;
|
|
|
|
var lastCmd = new DateTime(ticks, DateTimeKind.Utc);
|
|
double ms = (DateTime.UtcNow - lastCmd).TotalMilliseconds;
|
|
|
|
if (ms > FAILSAFE_MS)
|
|
{
|
|
// Para por segurança
|
|
GeneralJoystick.PararCarroControle("Muito tempo sem receber um novo comando em tmrFailSafe_Tick");
|
|
|
|
// desarma até chegar novo comando válido
|
|
Interlocked.Exchange(ref _lastValidCmdTicksUtc, 0);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _failSafeTickRunning, 0);
|
|
}
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// ===== WATCHDOG: se parou de chegar qualquer pacote, reinicia o socket =====
|
|
public static async Task tmrUdpWatchdog_Tick()
|
|
{
|
|
if (Interlocked.Exchange(ref _udpWatchdogTickRunning, 1) == 1)
|
|
return;
|
|
|
|
try
|
|
{
|
|
long ticks = Interlocked.Read(ref _lastPacketTicksUtc);
|
|
if (ticks == 0) return;
|
|
|
|
var lastPkt = new DateTime(ticks, DateTimeKind.Utc);
|
|
double ms = (DateTime.UtcNow - lastPkt).TotalMilliseconds;
|
|
|
|
if (ms > UDP_RESTART_MS)
|
|
{
|
|
try
|
|
{
|
|
await RestartUdpChannelSafe();
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
// normal durante restart
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
// normal durante restart
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("[UDP] Watchdog exception: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _udpWatchdogTickRunning, 0);
|
|
}
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
private static async Task RestartUdpChannelSafe()
|
|
{
|
|
if (Interlocked.Exchange(ref _udpRestarting, 1) == 1)
|
|
return;
|
|
|
|
try
|
|
{
|
|
try
|
|
{
|
|
if (_onPacketHandler != null)
|
|
UdpChannel.OnPacket -= _onPacketHandler;
|
|
}
|
|
catch { }
|
|
|
|
Console.WriteLine("[UDP] Tentando reconectar...");
|
|
// Reinicia o canal
|
|
StartUdpChannel();
|
|
|
|
// Importante: zera comando válido, porque “socket caiu” geralmente quebra o fluxo de comando
|
|
Interlocked.Exchange(ref _lastValidCmdTicksUtc, 0);
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _udpRestarting, 0);
|
|
}
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
|
|
|
|
public static void MostrarLog(string Mensagem)
|
|
{
|
|
Console.WriteLine($"[AGROBASE] {Mensagem}");
|
|
}
|
|
|
|
|
|
}
|
|
|
|
public static class VariaveisPortas
|
|
{
|
|
public static int Ethernet_AT { get; } = 8899;
|
|
public static int Ethernet_TCP { get; } = 8899;
|
|
public static int Ethernet_UDP_TX { get; } = 5005;
|
|
public static int Ethernet_UDP_RX { get; } = 5006;
|
|
}
|
|
|
|
public static class VariaveisEquipamento
|
|
{
|
|
private static ParametrosConfigEquipamentoModel _parametros;
|
|
public static ParametrosConfigEquipamentoModel Parametros
|
|
{
|
|
get
|
|
{
|
|
if (_parametros == null)
|
|
{
|
|
_parametros = new ParametrosConfigEquipamentoModel();
|
|
_parametros.AtualizarParametros();
|
|
}
|
|
return _parametros;
|
|
}
|
|
set
|
|
{
|
|
_parametros = value;
|
|
}
|
|
}
|
|
public static string TopicoMqttHeartbeat { get; } = $"agrobot/v1/rover/<id>/heartbeat";
|
|
public static string TopicoMqttComandos { get; } = $"agrobot/v1/rover/<id>/cmd";
|
|
public static string TopicoMqttTelemetria { get; } = $"agrobot/v1/rover/<id>/telemetry";
|
|
public static string TopicoMqttParametros { get; } = $"agrobot/v1/rover/<id>/parameters";
|
|
public static double LarguraEsquerda { get; } = 53.0; // 62
|
|
public static double LarguraDireita { get; } = 53.0; // 22
|
|
public static double ComprimentoFrente { get; } = 90.0; // 7
|
|
public static double ComprimentoTras { get; } = 40.0; // 107
|
|
public static double DistanciaEntreEixosCm { get; set; } = 92.0;
|
|
public static double LeverArmFrontalCm { get; set; } = 37.0; // cm
|
|
public static double LeverArmLateralCm { get; set; } = 0.0; // cm
|
|
public static double LarguraEquipamentoMm
|
|
{
|
|
get
|
|
{
|
|
return ((LarguraEsquerda + LarguraDireita + 10.0) * 10.0);
|
|
}
|
|
}
|
|
public static double TensaoMinimaBateria { get; set; } = 30.0;
|
|
public static double TensaoMaximaBateria { get; set; } = 42.0;
|
|
public static double CorrenteMaximaBateria { get; set; } = 100.0;
|
|
public static double PercentualTensaoBateriaMin { get; set; } = 25.0;
|
|
public static double PercentualReservatorioMin { get; set; } = 8.0;
|
|
public static double PercentualReservatorioMinCritico { get; set; } = 5.0;
|
|
public static double PercentualToleranciaPressaoLinha { get; set; } = 0.15;
|
|
public static int QuantidadeCamerasSolo { get; set; } = 1;
|
|
public static int QuantidadeBicosPulverizadores { get; set; } = 7;
|
|
public static double AlturaBarraPulverizadoraCm { get; set; } = 80;
|
|
public static double ComprimentoBarraPulverizadoraCm { get; set; } = 103.7;
|
|
public static double DistanciaEntreBicosCm { get; set; } = 21;
|
|
public static double DensidadeHerbicida { get; set; } = 1.0;
|
|
public static double CapacidadeReservatorio { get; set; } = 60;
|
|
public static double PressaoMinimaCavitacao { get; set; } = 10.0;
|
|
public static double ReducaoDirecional { get; set; } = 20.0;
|
|
public static double PercentualVelMin
|
|
{
|
|
get
|
|
{
|
|
return (RPM_Min_Roda / RPM_Max_Roda) * 100.0;
|
|
}
|
|
}
|
|
public static int RPM_Min_Roda { get; set; } = 15;
|
|
public static int RPM_Max_Roda
|
|
{
|
|
get
|
|
{
|
|
double RpmMax =
|
|
Variaveis.OperacaoEmAndamento?.DispMvd != null ?
|
|
Variaveis.OperacaoEmAndamento.DispMvd.Dados.RPM_Max_MotorBLDC / RelacaoRPM :
|
|
100;
|
|
return (int)RpmMax;
|
|
}
|
|
}
|
|
public static double RelacaoRPM
|
|
{
|
|
get
|
|
{
|
|
double RelacaoRPM =
|
|
Variaveis.OperacaoEmAndamento.DispMvd != null ?
|
|
Variaveis.OperacaoEmAndamento.DispMvd.Dados.ReducaoMotorBLDC :
|
|
1;
|
|
return RelacaoRPM;
|
|
}
|
|
}
|
|
public static double DiametroRoda
|
|
{
|
|
get
|
|
{
|
|
double diametro =
|
|
Variaveis.OperacaoEmAndamento.DispMvd != null ?
|
|
Variaveis.OperacaoEmAndamento.DispMvd.Dados.DiametroRoda :
|
|
0.3556;
|
|
return diametro;
|
|
}
|
|
}
|
|
public static int NumeroPolosMotor
|
|
{
|
|
get
|
|
{
|
|
double NPolos =
|
|
Variaveis.OperacaoEmAndamento.DispMvd != null ?
|
|
Variaveis.OperacaoEmAndamento.DispMvd.Dados.Modulos.Average(x => x.MovMotor.NumeroPolos) :
|
|
10;
|
|
return (int)NPolos;
|
|
}
|
|
}
|
|
public static int TempoEntrePingsConexao { get; set; } = 10000;
|
|
public static int TempoMaximoSalvarLogs
|
|
{
|
|
get
|
|
{
|
|
return TempoEntrePingsConexao * 2;
|
|
}
|
|
}
|
|
public static Dictionary<AcaoFreio, int> AnguloFreio { get; } = new Dictionary<AcaoFreio, int>()
|
|
{
|
|
{ AcaoFreio.Ativar, 15 },
|
|
{ AcaoFreio.Desativar, 0 }
|
|
};
|
|
public static Dictionary<TipoMovimentoDirecional, int> AnguloMovimento { get; } = new Dictionary<TipoMovimentoDirecional, int>()
|
|
{
|
|
{ TipoMovimentoDirecional.RodasDianteiras, -1 },
|
|
{ TipoMovimentoDirecional.RodasTraseiras, -1 },
|
|
{ TipoMovimentoDirecional.MovimentoDiagonal, -1 },
|
|
{ TipoMovimentoDirecional.MovimentoArco, -1 },
|
|
{ TipoMovimentoDirecional.RotacionarNoEixo, 45 },
|
|
{ TipoMovimentoDirecional.MovimentoLateral, 90 },
|
|
{ TipoMovimentoDirecional.Diagnostico, 25 },
|
|
};
|
|
public static double AnguloInclinacaoRollMax { get; set; } = 30.0; // Frontal 40 graus max
|
|
public static double AnguloInclinacaoPitchMax { get; set; } = 22.0; // Lateral
|
|
public static string VozAlerta { get; set; } = "Microsoft Maria Desktop";
|
|
public static bool Pronto => Variaveis.OperacaoEmAndamento?.Parametros?.Controle != null;
|
|
|
|
|
|
|
|
public static SensorSinaleiroComportamentoModel ComportamentoLedPorStatus(StatusLED status_led = StatusLED.Apagado, StatusOperacao? status_operacao = null)
|
|
{
|
|
SensorSinaleiroComportamentoModel comportamento = new SensorSinaleiroComportamentoModel()
|
|
{
|
|
id = 99,
|
|
statusOperacao = status_operacao,
|
|
statusLED = status_led,
|
|
QtdPiscadas = 0,
|
|
TempoMs = 0,
|
|
PausaMs = 0,
|
|
};
|
|
|
|
if (status_operacao == null) return comportamento;
|
|
|
|
comportamento.id = (int)((StatusOperacao)status_operacao);
|
|
|
|
switch ((StatusOperacao)status_operacao)
|
|
{
|
|
case StatusOperacao.Parametrizando:
|
|
{
|
|
comportamento.QtdPiscadas = 2;
|
|
comportamento.TempoMs = 800;
|
|
comportamento.PausaMs = 2000;
|
|
break;
|
|
}
|
|
case StatusOperacao.NaoIniciado:
|
|
case StatusOperacao.Concluido:
|
|
{
|
|
comportamento.QtdPiscadas = 1;
|
|
comportamento.TempoMs = 1000;
|
|
comportamento.PausaMs = 2000;
|
|
break;
|
|
}
|
|
case StatusOperacao.Aguardando:
|
|
{
|
|
comportamento.QtdPiscadas = 3;
|
|
comportamento.TempoMs = 500;
|
|
comportamento.PausaMs = 800;
|
|
break;
|
|
}
|
|
case StatusOperacao.Parado:
|
|
{
|
|
comportamento.QtdPiscadas = 5;
|
|
comportamento.TempoMs = 500;
|
|
comportamento.PausaMs = 2000;
|
|
break;
|
|
}
|
|
case StatusOperacao.EmAndamento:
|
|
{
|
|
comportamento.QtdPiscadas = 1;
|
|
comportamento.TempoMs = 500;
|
|
comportamento.PausaMs = 1000;
|
|
break;
|
|
}
|
|
case StatusOperacao.Calibrando:
|
|
{
|
|
comportamento.QtdPiscadas = 2;
|
|
comportamento.TempoMs = 500;
|
|
comportamento.PausaMs = 800;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return comportamento;
|
|
}
|
|
|
|
|
|
public static GPSModel SimularNovaPosicao_bkp(TipoMovimentoDirecional tipoMovimento, double anguloControle, double velocidadeMs, double tempoDelta, double anguloAtual, GPSModel PosicaoAtual)
|
|
{
|
|
// Fator de correção da curva
|
|
double k = 1.35 + 0.5 * Math.Exp(-Math.Abs(anguloControle) / 15.0);
|
|
|
|
// Converte o ângulo de controle para radianos
|
|
double anguloControleRad = anguloControle * (Math.PI / 180.0);
|
|
|
|
// Evita divisão por zero (caso o ângulo seja muito pequeno)
|
|
double R = Math.Abs(anguloControleRad) < 0.01 ? 99999 : (DistanciaEntreEixosCm / 100.0) / Math.Tan(anguloControleRad);
|
|
|
|
// Ajusta o raio de curva de acordo com o tipo de movimento
|
|
switch (tipoMovimento)
|
|
{
|
|
case TipoMovimentoDirecional.RodasDianteiras:
|
|
// Apenas as rodas dianteiras viram -> usa o raio padrão
|
|
break;
|
|
|
|
case TipoMovimentoDirecional.RodasTraseiras:
|
|
// Apenas as rodas traseiras viram -> mesmo efeito que as dianteiras
|
|
break;
|
|
|
|
case TipoMovimentoDirecional.MovimentoArco:
|
|
// Rodas dianteiras e traseiras viram em sentidos opostos, reduzindo muito o raio
|
|
R *= 0.5; // Reduz o raio pela metade para uma curva mais fechada
|
|
break;
|
|
|
|
case TipoMovimentoDirecional.MovimentoDiagonal:
|
|
// Todas as rodas viram na mesma direção, o robô desliza sem mudar a frente
|
|
// Isso significa que o ângulo do robô **não muda** ao longo do tempo
|
|
//return anguloAtual; // Mantém a orientação original
|
|
break;
|
|
}
|
|
|
|
// Aplica o fator de correção da curva
|
|
R *= k;
|
|
|
|
// Calcula a rotação angular com base na velocidade e no raio
|
|
double omega = velocidadeMs / R;
|
|
|
|
// Calcula a variação angular no tempo (em radianos)
|
|
double deltaTheta = omega * tempoDelta;
|
|
|
|
// Converte para graus e atualiza a orientação do robô
|
|
double novoAngulo = GPSUtils.NormalizarAngulo(anguloAtual + (deltaTheta * (180.0 / Math.PI)));
|
|
|
|
|
|
|
|
// Calcula a distância com base no tempo passado (em segundos)
|
|
double distancia = velocidadeMs * tempoDelta;
|
|
|
|
double latitude = PosicaoAtual.Latitude;
|
|
double longitude = PosicaoAtual.Longitude;
|
|
|
|
// Conversão de ângulo de graus para radianos
|
|
double anguloRad = novoAngulo * (Math.PI / 180);
|
|
|
|
// Raio da Terra em metros
|
|
double raioTerra = GPSUtils.RaioDaTerra;
|
|
|
|
// Calcular deslocamento de latitude em radianos
|
|
double deltaLat = distancia * Math.Cos(anguloRad) / raioTerra;
|
|
// Converter de radianos para graus
|
|
double _latAtt = latitude + deltaLat * (180 / Math.PI);
|
|
|
|
// Calcular deslocamento de longitude em radianos
|
|
double deltaLong = distancia * Math.Sin(anguloRad) / (raioTerra * Math.Cos(latitude * (Math.PI / 180)));
|
|
// Converter de radianos para graus
|
|
double _longAtt = longitude + deltaLong * (180 / Math.PI);
|
|
|
|
GPSModel novoPonto = new GPSModel()
|
|
{
|
|
Momento = DateTime.Now,
|
|
Latitude = _latAtt,
|
|
Longitude = _longAtt,
|
|
OrientacaoReal = novoAngulo,
|
|
Distancia = distancia,
|
|
Velocidade = velocidadeMs,
|
|
};
|
|
|
|
return novoPonto;
|
|
}
|
|
|
|
public static GPSModel SimularNovaPosicao(TipoMovimentoDirecional tipoMovimento, double anguloControleDeg, double velocidadeMs, double tempoDelta, double anguloAtualDeg, GPSModel pos)
|
|
{
|
|
double L = DistanciaEntreEixosCm / 100.0;
|
|
|
|
// Defina deltas por modo
|
|
double dfDeg = 0, drDeg = 0;
|
|
switch (tipoMovimento)
|
|
{
|
|
case TipoMovimentoDirecional.RodasDianteiras: dfDeg = anguloControleDeg; drDeg = 0; break;
|
|
case TipoMovimentoDirecional.RodasTraseiras: dfDeg = 0; drDeg = anguloControleDeg; break;
|
|
case TipoMovimentoDirecional.MovimentoArco: dfDeg = anguloControleDeg; drDeg = -anguloControleDeg; break;
|
|
case TipoMovimentoDirecional.MovimentoDiagonal: dfDeg = anguloControleDeg; drDeg = anguloControleDeg; break;
|
|
}
|
|
|
|
// Curvatura geométrica
|
|
double tf = Math.Tan(dfDeg * Math.PI / 180.0);
|
|
double tr = Math.Tan(drDeg * Math.PI / 180.0);
|
|
if (Math.Abs(tf) < 1e-6) tf = 0;
|
|
if (Math.Abs(tr) < 1e-6) tr = 0;
|
|
|
|
double kappa = 0; // 1/m
|
|
if (tipoMovimento == TipoMovimentoDirecional.RodasDianteiras) kappa = tf / L;
|
|
else if (tipoMovimento == TipoMovimentoDirecional.RodasTraseiras) kappa = tr / L;
|
|
else if (tipoMovimento == TipoMovimentoDirecional.MovimentoArco) kappa = (tf - tr) / L;
|
|
else /*Diagonal,Lateral*/ kappa = 0;
|
|
|
|
// Correção dinâmica (opcional)
|
|
double Ku = 2.5; // ajuste fino em campo
|
|
double kappaEf = kappa / (1 + Ku * velocidadeMs * velocidadeMs);
|
|
|
|
// Integração
|
|
double theta = anguloAtualDeg * Math.PI / 180.0;
|
|
double omega = velocidadeMs * kappaEf; // rad/s
|
|
double dtheta = omega * tempoDelta;
|
|
theta += dtheta;
|
|
|
|
double heading = theta;
|
|
if (new List<TipoMovimentoDirecional>() { TipoMovimentoDirecional.MovimentoDiagonal, TipoMovimentoDirecional.MovimentoLateral }.Contains(tipoMovimento))
|
|
heading = theta + dfDeg * Math.PI / 180.0; // crab: desloca na direção do steering
|
|
|
|
// >>> mesma convenção do Python: 0=Norte
|
|
double dx = velocidadeMs * tempoDelta * Math.Sin(heading); // Leste(+)
|
|
double dy = velocidadeMs * tempoDelta * Math.Cos(heading); // Norte(+)
|
|
|
|
GPSModel ultimaPosicao = GPSService.historicoPosicao.Peek();
|
|
if (pos == null) return ultimaPosicao;
|
|
|
|
// geo
|
|
double R_earth = GPSUtils.RaioDaTerra;
|
|
double dLat = (dy / R_earth) * 180.0 / Math.PI;
|
|
double dLon = (dx / (R_earth * Math.Cos(pos.Latitude * Math.PI / 180.0))) * 180.0 / Math.PI;
|
|
|
|
double latitude = pos.LatitudeAnt + dLat;
|
|
double longitude = pos.LongitudeAnt + dLon;
|
|
|
|
DateTime agora = DateTime.Now;
|
|
double agoraMono = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
|
|
|
|
double orientacaoRealDeg = GPSUtils.NormalizarAngulo(theta * 180.0 / Math.PI);
|
|
double orientacaoMovimentoDeg = GPSUtils.NormalizarAngulo(heading * 180.0 / Math.PI);
|
|
|
|
GPSModel novaPosicao = new GPSModel
|
|
{
|
|
Momento = agora,
|
|
DataHora = agora,
|
|
UltimoComandoRespondido = agora,
|
|
Lat0 = GPSService.UltimaLeitura.Lat0,
|
|
Lon0 = GPSService.UltimaLeitura.Lon0,
|
|
LatitudeAnt = latitude,
|
|
LongitudeAnt = longitude,
|
|
OrientacaoReal = orientacaoRealDeg,
|
|
AnguloCarroDefinido = orientacaoRealDeg,
|
|
OrientacaoMovimento = orientacaoMovimentoDeg,
|
|
TipoOrientacao = "A",
|
|
Distancia = velocidadeMs * tempoDelta,
|
|
Velocidade = velocidadeMs,
|
|
Heartbeat = ultimaPosicao.Heartbeat + 1,
|
|
TimestampOri = ultimaPosicao.TimestampOri.Clone(),
|
|
TimestampPos = ultimaPosicao.TimestampPos.Clone(),
|
|
};
|
|
|
|
var corrigida =
|
|
GPSService.LeverArm.FixLeverArmLatLon_Fast(
|
|
novaPosicao.LatitudeAnt,
|
|
novaPosicao.LongitudeAnt,
|
|
novaPosicao.OrientacaoReal,
|
|
5
|
|
);
|
|
novaPosicao.Latitude = corrigida.lat;
|
|
novaPosicao.Longitude = corrigida.lon;
|
|
|
|
//(double latCor, double lonCorr) = GeoLeverArm.FixLeverArmLatLon_Fast(latitude, longitude, novaPosicao.OrientacaoReal);
|
|
//novaPosicao.Latitude = latCor;
|
|
//novaPosicao.Longitude = lonCorr;
|
|
|
|
novaPosicao.TimestampOri.valor = agoraMono;
|
|
novaPosicao.TimestampPos.valor = agoraMono;
|
|
|
|
return novaPosicao;
|
|
}
|
|
|
|
|
|
|
|
public class ParametrosConfigEquipamentoModel
|
|
{
|
|
public bool Carregado { get; set; }
|
|
public string serial_number { get; set; }
|
|
public string base_ip { get; set; }
|
|
public int base_porta_caminho { get; set; }
|
|
public int base_porta_ervas { get; set; }
|
|
public string comunicacao_interface { get; set; }
|
|
public string rover_ip { get; set; }
|
|
public string rover_gateway { get; set; }
|
|
public string rover_submask { get; set; }
|
|
public string camera_caminhho_id { get; set; }
|
|
public List<string> camera_ervas_id { get; set; }
|
|
public string can_interface { get; set; }
|
|
public string can_ip_pc { get; set; }
|
|
public string can_ip_modulo { get; set; }
|
|
public int can_porta { get; set; }
|
|
public string can_gateway { get; set; }
|
|
public string can_submask { get; set; }
|
|
|
|
public void AtualizarParametros()
|
|
{
|
|
string config_path = Path.Combine(Variaveis.CaminhoSistema, Variaveis.CaminhoParametros, "config.json");
|
|
if (!File.Exists(config_path))
|
|
Carregado = false;
|
|
|
|
try
|
|
{
|
|
string json = File.ReadAllText(config_path);
|
|
var par = JsonConvert.DeserializeObject<ParametrosConfigEquipamentoModel>(json);
|
|
|
|
serial_number = par.serial_number;
|
|
base_ip = par.base_ip;
|
|
base_porta_caminho = par.base_porta_caminho;
|
|
base_porta_ervas = par.base_porta_ervas;
|
|
|
|
comunicacao_interface = par.comunicacao_interface;
|
|
rover_ip = par.rover_ip;
|
|
rover_gateway = par.rover_gateway;
|
|
rover_submask = par.rover_submask;
|
|
|
|
can_interface = par.can_interface;
|
|
can_ip_pc = par.can_ip_pc;
|
|
can_ip_modulo = par.can_ip_modulo;
|
|
can_porta = par.can_porta;
|
|
can_gateway = par.can_gateway;
|
|
can_submask = par.can_submask;
|
|
|
|
camera_caminhho_id = par.camera_caminhho_id;
|
|
camera_ervas_id = par.camera_ervas_id;
|
|
|
|
Carregado = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Erro ao carregar arquivo de configuracoes: {ex.Message}");
|
|
Carregado = false;
|
|
}
|
|
}
|
|
|
|
public bool SalvarParametros()
|
|
{
|
|
try
|
|
{
|
|
string config_path = Path.Combine(Variaveis.CaminhoSistema, Variaveis.CaminhoParametros, "config.json");
|
|
File.WriteAllText(config_path, JsonConvert.SerializeObject(this));
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Erro ao salvar arquivo de configuracoes: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public static class VariaveisOperacao
|
|
{
|
|
public static double TempoManobra { get; set; } = 10;
|
|
private static readonly object _posicaoBaseLock = new object();
|
|
private static GPSModel _posicaoBase = new GPSModel();
|
|
|
|
/// <summary>
|
|
/// Propriedade mantida por compatibilidade. Novos callbacks devem usar
|
|
/// AtualizarPosicaoBase e leituras concorrentes devem usar
|
|
/// ObterPosicaoBaseSnapshot.
|
|
/// </summary>
|
|
public static GPSModel PosicaoBase
|
|
{
|
|
get
|
|
{
|
|
lock (_posicaoBaseLock)
|
|
return _posicaoBase;
|
|
}
|
|
set
|
|
{
|
|
lock (_posicaoBaseLock)
|
|
_posicaoBase = value ?? new GPSModel();
|
|
}
|
|
}
|
|
|
|
public static void AtualizarPosicaoBase(GPSModel posicao)
|
|
{
|
|
if (posicao == null)
|
|
return;
|
|
|
|
lock (_posicaoBaseLock)
|
|
{
|
|
// Compatibilidade temporária: a lógica antiga de discovery
|
|
// ainda consulta este campo. Uma posição nova não deve apagar
|
|
// o heartbeat recebido anteriormente.
|
|
DateTime heartbeatAnterior =
|
|
_posicaoBase?.UltimoComandoRespondido ?? DateTime.MinValue;
|
|
|
|
if (posicao.UltimoComandoRespondido < heartbeatAnterior)
|
|
posicao.UltimoComandoRespondido = heartbeatAnterior;
|
|
|
|
_posicaoBase = posicao;
|
|
}
|
|
}
|
|
|
|
public static void MarcarHeartbeatBaseLegado(DateTime momento)
|
|
{
|
|
lock (_posicaoBaseLock)
|
|
{
|
|
if (_posicaoBase == null)
|
|
_posicaoBase = new GPSModel();
|
|
|
|
_posicaoBase.UltimoComandoRespondido = momento;
|
|
}
|
|
}
|
|
|
|
public static GPSModel ObterPosicaoBaseSnapshot()
|
|
{
|
|
lock (_posicaoBaseLock)
|
|
return _posicaoBase?.Clone() ?? new GPSModel();
|
|
}
|
|
|
|
public static OperadoresService Operadores { get; set; } = new OperadoresService();
|
|
|
|
public static void RegistrarLogDispositivo(List<string> Logs, T_Code Dispositivo, string Sulfixo, int MinLogs = 100)
|
|
{
|
|
var Caminho = Path.Combine(Variaveis.CaminhoLogsDispositivos, Dispositivo.ToString());
|
|
|
|
string NomeArquivo = Dispositivo.ToString() + Sulfixo;
|
|
|
|
var Disp = SerialService.DispositivosMapeados.FirstOrDefault(x => x.Dispositivo == Dispositivo);
|
|
|
|
try
|
|
{
|
|
if (Disp != null)
|
|
{
|
|
NomeArquivo = Disp.CriadoEm.ToString("dd_MM_yyyy_HH_mm_ss") + Sulfixo;
|
|
}
|
|
|
|
if (Logs.Count() >= MinLogs)
|
|
{
|
|
if (!Directory.Exists(Caminho))
|
|
{
|
|
Directory.CreateDirectory(Caminho);
|
|
}
|
|
|
|
Caminho = Path.Combine(Caminho, NomeArquivo);
|
|
|
|
if (!File.Exists(Caminho))
|
|
{
|
|
File.WriteAllLines(Caminho, Logs);
|
|
}
|
|
else
|
|
{
|
|
File.AppendAllLines(Caminho, Logs);
|
|
}
|
|
|
|
Logs.Clear();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Console.WriteLine("Erro ao salvar log: " + NomeArquivo);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public static class FuncoesGlobais
|
|
{
|
|
public static void AddApplicationToStartup()
|
|
{
|
|
string appName = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyTitleAttribute>()?.Title ?? "DefaultAppName";
|
|
string appPath = Application.ExecutablePath;
|
|
|
|
RegistryKey registryKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true);
|
|
if (registryKey.GetValue(appName) == null)
|
|
{
|
|
registryKey.SetValue(appName, appPath);
|
|
}
|
|
}
|
|
|
|
public static string TimestampToDate(double timestamp, string Formatacao = "HH:mm:ss.fff")
|
|
{
|
|
// Converta o timestamp Unix para DateTime
|
|
// A época Unix começa em 1 de janeiro de 1970
|
|
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
DateTime date = epoch.AddSeconds(timestamp);
|
|
|
|
// Converter a data UTC para a data no fuso horário local do sistema
|
|
DateTime localDate = date.ToLocalTime();
|
|
|
|
// Formate a data para hh:mm:ss.fff (note a correção de MM para mm)
|
|
string formattedDate = localDate.ToString(Formatacao, System.Globalization.CultureInfo.InvariantCulture);
|
|
|
|
return formattedDate;
|
|
}
|
|
|
|
public static T FindControlRecursive<T>(Control parent, string controlName) where T : Control
|
|
{
|
|
foreach (Control child in parent.Controls)
|
|
{
|
|
if (child is T foundControl && child.Name == controlName)
|
|
{
|
|
return foundControl; // Retorna o controle se encontrado
|
|
}
|
|
else
|
|
{
|
|
T foundInChildren = FindControlRecursive<T>(child, controlName);
|
|
if (foundInChildren != null) return foundInChildren; // Retorna o controle se encontrado em controles filhos
|
|
}
|
|
}
|
|
return null; // Retorna null se o controle não for encontrado
|
|
}
|
|
|
|
public static void PreencheValorProgressBar(ProgressBar pgb, double Valor)
|
|
{
|
|
pgb.Value =
|
|
Valor >= pgb.Minimum && Valor <= pgb.Maximum ? pgb.Value = Convert.ToInt32(Valor) :
|
|
Valor < pgb.Minimum ? pgb.Value = pgb.Minimum :
|
|
Valor > pgb.Maximum ? pgb.Value = pgb.Maximum :
|
|
pgb.Minimum;
|
|
}
|
|
|
|
public static void PreencheValorProgressBar(HmiProgressBar pgb, double Valor)
|
|
{
|
|
pgb.Value =
|
|
Valor >= pgb.Minimum && Valor <= pgb.Maximum ? pgb.Value = Convert.ToInt32(Valor) :
|
|
Valor < pgb.Minimum ? pgb.Value = pgb.Minimum :
|
|
Valor > pgb.Maximum ? pgb.Value = pgb.Maximum :
|
|
pgb.Minimum;
|
|
}
|
|
|
|
public static void PreencheValorTrackBar(TrackBar tkb, double Valor)
|
|
{
|
|
tkb.Value =
|
|
Valor >= tkb.Minimum && Valor <= tkb.Maximum ? tkb.Value = Convert.ToInt32(Valor) :
|
|
Valor < tkb.Minimum ? tkb.Value = tkb.Minimum :
|
|
Valor > tkb.Maximum ? tkb.Value = tkb.Maximum :
|
|
tkb.Minimum;
|
|
}
|
|
|
|
|
|
// Método para salvar o frame atual em JPEG
|
|
public static void SalvarFrameJPEG(Bitmap frame, string caminhoArquivo)
|
|
{
|
|
// Define as opções de codificação JPEG (qualidade de 90%)
|
|
EncoderParameters encoderParams = new EncoderParameters(1);
|
|
encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 90L);
|
|
|
|
// Salva o bitmap em formato JPEG
|
|
ImageCodecInfo jpegCodec = GetEncoderInfo(ImageFormat.Jpeg);
|
|
frame.Save(caminhoArquivo, jpegCodec, encoderParams);
|
|
}
|
|
|
|
// Método auxiliar para obter informações do codec JPEG
|
|
private static ImageCodecInfo GetEncoderInfo(ImageFormat format)
|
|
{
|
|
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
|
|
foreach (ImageCodecInfo codec in codecs)
|
|
{
|
|
if (codec.FormatID == format.Guid)
|
|
{
|
|
return codec;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static ImageCodecInfo GetEncoderInfo(string mimeType)
|
|
{
|
|
// Obtém todos os codecs de imagem disponíveis
|
|
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
|
|
// Procura pelo codec que corresponde ao mimeType fornecido
|
|
foreach (ImageCodecInfo codec in codecs)
|
|
{
|
|
if (codec.MimeType == mimeType)
|
|
{
|
|
return codec;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
public static Image EspelharImagemHorizontalmente(Image img)
|
|
{
|
|
// Cria uma cópia da imagem do PictureBox
|
|
Bitmap originalImage = new Bitmap(img);
|
|
|
|
// Cria uma nova imagem para conter a imagem espelhada
|
|
Bitmap mirroredImage = new Bitmap(originalImage.Width, originalImage.Height);
|
|
|
|
// Cria uma matriz de transformação para espelhar horizontalmente
|
|
Matrix matrix = new Matrix();
|
|
matrix.Scale(-1, 1);
|
|
|
|
// Cria um objeto Graphics para desenhar a imagem espelhada
|
|
using (Graphics g = Graphics.FromImage(mirroredImage))
|
|
{
|
|
// Espelha a imagem original na nova imagem
|
|
g.Transform = matrix;
|
|
g.DrawImage(originalImage, new Rectangle(-originalImage.Width, 0, originalImage.Width, originalImage.Height));
|
|
}
|
|
|
|
return mirroredImage;
|
|
}
|
|
|
|
// Método para rotacionar a imagem
|
|
public static Image RotacionarImagem(Image img, double rotationAngle, bool redimensionarParaCaber = true)
|
|
{
|
|
// Calcula as novas dimensões do bitmap com base na rotação
|
|
double angleRad = rotationAngle / 180.0 * Math.PI;
|
|
double sin = Math.Abs(Math.Sin(angleRad));
|
|
double cos = Math.Abs(Math.Cos(angleRad));
|
|
int newWidth = (int)(img.Width * cos + img.Height * sin);
|
|
int newHeight = (int)(img.Width * sin + img.Height * cos);
|
|
|
|
Bitmap bmp;
|
|
|
|
if (redimensionarParaCaber)
|
|
{
|
|
// Cria um novo bitmap com as dimensões calculadas
|
|
bmp = new Bitmap(newWidth, newHeight);
|
|
}
|
|
else
|
|
{
|
|
// Cria um novo bitmap com as dimensões originais
|
|
bmp = new Bitmap(img.Width, img.Height);
|
|
}
|
|
|
|
using (Graphics gfx = Graphics.FromImage(bmp))
|
|
{
|
|
gfx.Clear(Color.Transparent); // Define a cor de fundo como transparente
|
|
|
|
if (redimensionarParaCaber)
|
|
{
|
|
gfx.TranslateTransform(newWidth / 2f, newHeight / 2f); // Move o ponto de rotação para o centro do novo bitmap
|
|
}
|
|
else
|
|
{
|
|
gfx.TranslateTransform(img.Width / 2f, img.Height / 2f); // Move o ponto de rotação para o centro do bitmap original
|
|
}
|
|
|
|
gfx.RotateTransform((float)rotationAngle); // Aplica a rotação
|
|
|
|
if (redimensionarParaCaber)
|
|
{
|
|
gfx.TranslateTransform(-img.Width / 2f, -img.Height / 2f); // Move a imagem de volta para o centro
|
|
}
|
|
else
|
|
{
|
|
gfx.TranslateTransform(-img.Width / 2f, -img.Height / 2f); // Move a imagem de volta para o centro
|
|
}
|
|
|
|
gfx.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
|
gfx.DrawImage(img, 0, 0, img.Width, img.Height); // Desenha a imagem original no bitmap
|
|
}
|
|
|
|
return bmp;
|
|
}
|
|
|
|
public static void DesenharInclinacao(Panel pnlBussola, PaintEventArgs e, float anguloInclinacao, float anguloSub, float? anguloOpcional = null)
|
|
{
|
|
if (!(anguloInclinacao >= 0 || anguloInclinacao <= 0))
|
|
{
|
|
anguloInclinacao = 0;
|
|
}
|
|
|
|
Graphics g = e.Graphics;
|
|
// Desenha o círculo da bússola
|
|
int diameter = Math.Min(pnlBussola.Width - 1, pnlBussola.Height - 1);
|
|
Point centro = new Point((pnlBussola.Width - 1) / 2, (pnlBussola.Height - 1) / 2);
|
|
//g.DrawEllipse(Pens.Black, centro.X - diameter / 2, centro.Y - diameter / 2, diameter, diameter);
|
|
|
|
// Escreve o ângulo no centro do círculo
|
|
string anguloTexto = (anguloOpcional.HasValue ? "C: " : "") + $"{anguloInclinacao.ToString("0.0")}°";
|
|
Font fonteAngulo = new Font("Arial", 7); // Ajuste o tamanho da fonte conforme necessário
|
|
SizeF textSize = e.Graphics.MeasureString(anguloTexto, fonteAngulo);
|
|
|
|
// Calcula a posição para o texto ser centralizado no círculo
|
|
float textX = 0;
|
|
float textY = 0;
|
|
|
|
// Define a cor do texto
|
|
Brush textBrush = Brushes.Black;
|
|
|
|
// Desenha o texto no Graphics do painel
|
|
g.DrawString(anguloTexto, fonteAngulo, textBrush, textX, textY);
|
|
|
|
// A linha será desenhada do centro do círculo
|
|
PointF pontoCentral = new PointF(centro.X, centro.Y);
|
|
// O comprimento da linha será metade do diâmetro do círculo (raio)
|
|
float comprimentoLinha = diameter / 3;
|
|
|
|
// Ajustar o ângulo para que 0 graus esteja para cima e aumente no sentido horário
|
|
//float anguloRadianos = (anguloInclinacao - 90) * (float)(Math.PI / 180.0);
|
|
float anguloRadianos = (anguloInclinacao - anguloSub) * (float)(Math.PI / 180.0);
|
|
|
|
// Calcular o ponto final da linha baseado no ângulo de inclinação
|
|
PointF pontoFinal = new PointF(
|
|
pontoCentral.X + comprimentoLinha * (float)Math.Cos(anguloRadianos),
|
|
pontoCentral.Y + comprimentoLinha * (float)Math.Sin(anguloRadianos)
|
|
);
|
|
|
|
// Desenhar a linha de inclinação
|
|
g.DrawLine(Pens.Red, pontoCentral, pontoFinal);
|
|
|
|
// Opcional: Desenhar uma ponta de seta na linha para indicar direção
|
|
using (AdjustableArrowCap bigArrow = new AdjustableArrowCap(4, 4))
|
|
{
|
|
using (Pen pen = new Pen(Color.Red, 2))
|
|
{
|
|
pen.CustomEndCap = bigArrow;
|
|
g.DrawLine(pen, pontoCentral, pontoFinal);
|
|
}
|
|
}
|
|
|
|
|
|
// Desenhar a segunda seta se o ângulo opcional for fornecido
|
|
if (anguloOpcional.HasValue)
|
|
{
|
|
// Escreve o ângulo no centro do círculo
|
|
string anguloTexto2 = $"R: {anguloOpcional.Value.ToString("0.0")}°";
|
|
SizeF textSize2 = e.Graphics.MeasureString(anguloTexto2, fonteAngulo);
|
|
|
|
// Calcula a posição para o texto ser centralizado no círculo
|
|
float textX2 = pnlBussola.Width - textSize2.Width;
|
|
float textY2 = 0;
|
|
|
|
// Desenha o texto no Graphics do painel
|
|
g.DrawString(anguloTexto2, fonteAngulo, Brushes.Blue, textX2, textY2);
|
|
|
|
float anguloOpcionalRadianos = (anguloOpcional.Value - anguloSub) * (float)(Math.PI / 180.0);
|
|
|
|
PointF pontoFinalOpcional = new PointF(
|
|
pontoCentral.X + comprimentoLinha * (float)Math.Cos(anguloOpcionalRadianos),
|
|
pontoCentral.Y + comprimentoLinha * (float)Math.Sin(anguloOpcionalRadianos)
|
|
);
|
|
|
|
using (AdjustableArrowCap bigArrow = new AdjustableArrowCap(4, 4))
|
|
{
|
|
using (Pen pen = new Pen(Color.Blue, 2))
|
|
{
|
|
pen.CustomEndCap = bigArrow;
|
|
g.DrawLine(pen, pontoCentral, pontoFinalOpcional);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static Bitmap FazerOverlay(Bitmap rgb, Bitmap segmentada, float alpha = 0.35f, bool useNearest = true)
|
|
{
|
|
// 1) Garantir mesma resolução
|
|
Bitmap segSameSize = segmentada;
|
|
if (segmentada.Width != rgb.Width || segmentada.Height != rgb.Height)
|
|
{
|
|
segSameSize = new Bitmap(rgb.Width, rgb.Height, PixelFormat.Format24bppRgb);
|
|
using (var g = Graphics.FromImage(segSameSize))
|
|
{
|
|
g.InterpolationMode = useNearest ? InterpolationMode.NearestNeighbor : InterpolationMode.HighQualityBilinear;
|
|
g.PixelOffsetMode = PixelOffsetMode.Half;
|
|
g.DrawImage(segmentada, new Rectangle(0, 0, rgb.Width, rgb.Height));
|
|
}
|
|
}
|
|
|
|
// 2) Compor overlay (rgb + alpha*segmentada)
|
|
var output = new Bitmap(rgb.Width, rgb.Height, PixelFormat.Format24bppRgb);
|
|
using (var g = Graphics.FromImage(output))
|
|
using (var ia = new ImageAttributes())
|
|
{
|
|
// fundo (RGB)
|
|
g.DrawImage(rgb, 0, 0, rgb.Width, rgb.Height);
|
|
|
|
// matriz de cor com alpha global
|
|
var cm = new ColorMatrix
|
|
{
|
|
Matrix00 = 1f,
|
|
Matrix11 = 1f,
|
|
Matrix22 = 1f, // R,G,B inalterados
|
|
Matrix33 = alpha, // A (transparência da segmentação)
|
|
Matrix44 = 1f
|
|
};
|
|
ia.SetColorMatrix(cm, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
|
|
|
|
// overlay = 0.65*rgb + 0.35*seg (se quiser pesar o fundo, desenhe rgb antes, como já fizemos)
|
|
g.CompositingMode = CompositingMode.SourceOver;
|
|
g.CompositingQuality = CompositingQuality.HighSpeed;
|
|
g.InterpolationMode = useNearest ? InterpolationMode.NearestNeighbor : InterpolationMode.HighQualityBilinear;
|
|
g.PixelOffsetMode = PixelOffsetMode.Half;
|
|
|
|
g.DrawImage(
|
|
segSameSize,
|
|
new Rectangle(0, 0, rgb.Width, rgb.Height),
|
|
0, 0, segSameSize.Width, segSameSize.Height,
|
|
GraphicsUnit.Pixel,
|
|
ia
|
|
);
|
|
}
|
|
|
|
if (!ReferenceEquals(segSameSize, segmentada))
|
|
segSameSize.Dispose();
|
|
|
|
return output;
|
|
}
|
|
|
|
public static string ConverterSegundosParaHHmmss(int totalSeconds)
|
|
{
|
|
TimeSpan timeSpan = TimeSpan.FromSeconds(totalSeconds);
|
|
return timeSpan.ToString(@"hh\:mm\:ss");
|
|
}
|
|
|
|
public static async Task<bool> AguardarCondicaoAsync(Func<bool> condicao, int timeoutMs = 5000, int delayMs = 200, Func<bool> forcarLiberacao = null)
|
|
{
|
|
var tempoInicial = DateTime.Now;
|
|
|
|
while (!condicao())
|
|
{
|
|
// Se atingiu o timeout, interrompe e retorna false
|
|
if ((DateTime.Now - tempoInicial).TotalMilliseconds >= timeoutMs)
|
|
return false;
|
|
|
|
// Se a variável de liberação for passada e retornar true, interrompe o loop
|
|
if (forcarLiberacao != null && forcarLiberacao())
|
|
return true;
|
|
|
|
await Task.Delay(delayMs);
|
|
}
|
|
|
|
return true; // Condição atingida dentro do tempo limite
|
|
}
|
|
|
|
|
|
public static void DefinirEventosPicBtn(Control ctrl, string mensagemErro, Func<Task> func, bool hover = false)
|
|
{
|
|
ctrl.Cursor = Cursors.Hand;
|
|
|
|
// Adiciona eventos de hover (evita múltiplos registros)
|
|
if (hover)
|
|
{
|
|
ctrl.MouseEnter -= picBtn_MouseEnter;
|
|
ctrl.MouseLeave -= picBtn_MouseLeave;
|
|
ctrl.MouseEnter += picBtn_MouseEnter;
|
|
ctrl.MouseLeave += picBtn_MouseLeave;
|
|
}
|
|
|
|
// Remove eventos anteriores e adiciona o novo
|
|
ctrl.Click -= async (s, args) => await HandlePicBtnClick(ctrl, mensagemErro, func);
|
|
ctrl.Click += async (s, args) => await HandlePicBtnClick(ctrl, mensagemErro, func);
|
|
}
|
|
|
|
private static async Task HandlePicBtnClick(Control ctrl, string mensagemErro, Func<Task> func)
|
|
{
|
|
try
|
|
{
|
|
// Executa o efeito de piscada antes da ação
|
|
await PiscarBtn(ctrl, 2);
|
|
|
|
// Executa a ação associada ao botão
|
|
if (func != null)
|
|
{
|
|
await func();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Exibe uma mensagem de erro genérica para qualquer exceção
|
|
CustomDialog.ShowDialog(
|
|
"Erro",
|
|
$"{mensagemErro}\nDetalhes: {ex.Message}",
|
|
MessageBoxIcon.Error,
|
|
new Dictionary<(string, dynamic), Action>()
|
|
{
|
|
{ ("OK", null), () => { } },
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
public static void picBtn_MouseEnter(object sender, EventArgs e)
|
|
{
|
|
Task.Run(async () => await TransicaoTamanho((Control)sender, true, 5));
|
|
}
|
|
|
|
public static void picBtn_MouseLeave(object sender, EventArgs e)
|
|
{
|
|
Task.Run(async () => await TransicaoTamanho((Control)sender, false, 5));
|
|
}
|
|
|
|
private static void AlteraTamanhoObjeto(Control ctrl, bool Cresce)
|
|
{
|
|
int acX = Convert.ToInt32(ctrl.Width * 0.06);
|
|
if (acX % 2 == 1) acX++;
|
|
int acY = Convert.ToInt32(ctrl.Height * 0.06);
|
|
if (acY % 2 == 1) acY++;
|
|
if (!Cresce)
|
|
{
|
|
acX *= -1;
|
|
acY *= -1;
|
|
}
|
|
|
|
ctrl.Size = new Size((int)(ctrl.Width + acX), (int)(ctrl.Height + acY));
|
|
ctrl.Location = new Point(ctrl.Location.X - (acX / 2), ctrl.Location.Y - (acY / 2));
|
|
}
|
|
|
|
private static async Task TransicaoTamanho(Control ctrl, bool Cresce, int delay)
|
|
{
|
|
int steps = 10; // Número de passos na transição
|
|
int acX = Convert.ToInt32(ctrl.Width * 0.06);
|
|
if (acX % 2 == 1) acX++;
|
|
int acY = Convert.ToInt32(ctrl.Height * 0.06);
|
|
if (acY % 2 == 1) acY++;
|
|
|
|
// Ajuste para garantir que stepX e stepY sejam pelo menos 2
|
|
int stepX = Math.Max(acX / steps, 2);
|
|
int stepY = Math.Max(acY / steps, 2);
|
|
if (!Cresce)
|
|
{
|
|
stepX = -stepX;
|
|
stepY = -stepY;
|
|
}
|
|
|
|
for (int i = 0; i < steps; i++)
|
|
{
|
|
ctrl.Invoke((MethodInvoker)(() =>
|
|
{
|
|
ctrl.Size = new Size(ctrl.Width + stepX, ctrl.Height + stepY);
|
|
ctrl.Location = new Point(ctrl.Location.X - (stepX / 2), ctrl.Location.Y - (stepY / 2));
|
|
}));
|
|
await Task.Delay(delay);
|
|
}
|
|
}
|
|
|
|
private static async Task PiscarBtn(Control ctrl, int vezes)
|
|
{
|
|
for (int i = 0; i < vezes; i++)
|
|
{
|
|
await TransicaoTamanho(ctrl, true, 1);
|
|
await Task.Delay(3);
|
|
await TransicaoTamanho(ctrl, false, 1);
|
|
await Task.Delay(3);
|
|
}
|
|
}
|
|
|
|
public static Bitmap GetPanelImage(Panel panel)
|
|
{
|
|
// Cria um bitmap do tamanho do panel
|
|
Bitmap bitmap = new Bitmap(panel.Width, panel.Height);
|
|
|
|
// Desenha o conteúdo do panel no bitmap
|
|
panel.DrawToBitmap(bitmap, new Rectangle(0, 0, panel.Width, panel.Height));
|
|
|
|
// Retorna o bitmap
|
|
return bitmap;
|
|
}
|
|
|
|
public static async Task<Bitmap> GetBrowserImage(WebView2 browser)
|
|
{
|
|
if (browser?.CoreWebView2 == null)
|
|
return null;
|
|
|
|
using (var stream = new MemoryStream())
|
|
{
|
|
// Captura em PNG
|
|
await browser.CoreWebView2.CapturePreviewAsync(CoreWebView2CapturePreviewImageFormat.Png, stream);
|
|
|
|
stream.Position = 0;
|
|
return new Bitmap(stream);
|
|
}
|
|
}
|
|
|
|
|
|
public static void SalvarImagemComprimida(Bitmap frame, string Caminho, long qualidade)
|
|
{
|
|
// Se o frame for nulo, cria um novo bitmap representando uma tela preta
|
|
if (frame == null)
|
|
{
|
|
frame = new Bitmap(1, 1);
|
|
using (Graphics g = Graphics.FromImage(frame))
|
|
{
|
|
g.Clear(Color.Black);
|
|
}
|
|
}
|
|
|
|
// Obtém o codec para JPEG
|
|
ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg");
|
|
|
|
// Configura os parâmetros de compressão
|
|
EncoderParameters encoderParams = new EncoderParameters(1);
|
|
encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, qualidade);
|
|
|
|
using (Graphics g = Graphics.FromImage(frame))
|
|
{
|
|
g.DrawString(DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"), new Font("Arial", 10), Brushes.Transparent, new PointF(10, 10));
|
|
}
|
|
|
|
// Salva a imagem com a qualidade ajustada
|
|
frame.Save(Caminho, jpegCodec, encoderParams);
|
|
}
|
|
|
|
public static void SalvarImagemComprimida(Bitmap frame, string Caminho, long qualidade, int larguraDestino, int alturaDestino)
|
|
{
|
|
// Se o frame for nulo, cria um novo bitmap representando uma tela preta
|
|
if (frame == null)
|
|
{
|
|
larguraDestino = 1;
|
|
alturaDestino = 1;
|
|
frame = new Bitmap(larguraDestino, alturaDestino);
|
|
using (Graphics g = Graphics.FromImage(frame))
|
|
{
|
|
g.Clear(Color.Black);
|
|
}
|
|
}
|
|
|
|
Color corBorda = Color.FromArgb(14, 14, 14);
|
|
|
|
// Remover bordas pretas
|
|
Bitmap frameSemBordas = RemoverBordasPretas(frame, corBorda);
|
|
|
|
// Criar o novo bitmap redimensionado para o tamanho de destino
|
|
Bitmap bitmapRedimensionado = new Bitmap(larguraDestino, alturaDestino);
|
|
|
|
// Redimensionar a imagem original para ocupar toda a área de 512x512
|
|
using (Graphics g = Graphics.FromImage(bitmapRedimensionado))
|
|
{
|
|
// Configurações de alta qualidade para minimizar a perda de qualidade durante o redimensionamento
|
|
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
|
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
|
|
|
// Preencher o bitmap de destino (512x512) com a imagem redimensionada
|
|
g.DrawImage(frameSemBordas, 0, 0, larguraDestino, alturaDestino);
|
|
|
|
// Adiciona a data e hora na imagem (opcional)
|
|
g.DrawString(DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"), new Font("Arial", 10), Brushes.Transparent, new PointF(10, 10));
|
|
}
|
|
|
|
// Obtém o codec para JPEG
|
|
ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg");
|
|
|
|
// Configura os parâmetros de compressão
|
|
EncoderParameters encoderParams = new EncoderParameters(1);
|
|
encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, qualidade);
|
|
|
|
// Salva a imagem redimensionada com a qualidade ajustada
|
|
bitmapRedimensionado.Save(Caminho, jpegCodec, encoderParams);
|
|
|
|
// Libera os recursos usados pelo bitmap redimensionado
|
|
bitmapRedimensionado.Dispose();
|
|
}
|
|
|
|
private static Bitmap RemoverBordasPretas(Bitmap _imagem, Color corBorda)
|
|
{
|
|
Bitmap imagem = (Bitmap)_imagem.Clone();
|
|
|
|
int xMin = imagem.Width, xMax = 0, yMin = imagem.Height, yMax = 0;
|
|
|
|
// Iterar sobre todos os pixels para encontrar os limites da imagem sem bordas com a cor especificada
|
|
for (int y = 0; y < imagem.Height; y++)
|
|
{
|
|
for (int x = 0; x < imagem.Width; x++)
|
|
{
|
|
Color pixel = imagem.GetPixel(x, y);
|
|
// Verificar se o pixel não corresponde à cor da borda
|
|
if (pixel.R != corBorda.R || pixel.G != corBorda.G || pixel.B != corBorda.B)
|
|
{
|
|
if (x < xMin) xMin = x;
|
|
if (x > xMax) xMax = x;
|
|
if (y < yMin) yMin = y;
|
|
if (y > yMax) yMax = y;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Se a imagem for completamente da cor da borda, retornar um bitmap vazio
|
|
if (xMax == 0 && yMax == 0)
|
|
{
|
|
return new Bitmap(1, 1); // Imagem completamente da cor da borda
|
|
}
|
|
|
|
// Cortar a imagem para remover as bordas
|
|
int novaLargura = xMax - xMin + 1;
|
|
int novaAltura = yMax - yMin + 1;
|
|
|
|
Bitmap imagemSemBordas = new Bitmap(novaLargura, novaAltura);
|
|
using (Graphics g = Graphics.FromImage(imagemSemBordas))
|
|
{
|
|
g.DrawImage(imagem, new Rectangle(0, 0, novaLargura, novaAltura), new Rectangle(xMin, yMin, novaLargura, novaAltura), GraphicsUnit.Pixel);
|
|
}
|
|
|
|
return imagemSemBordas;
|
|
}
|
|
|
|
public static async Task ExecutarMetodoComVerificacaoCrossThreadAsync(Control controle, Func<Task> acao)
|
|
{
|
|
if (controle.InvokeRequired)
|
|
{
|
|
var tcs = new TaskCompletionSource<object>();
|
|
|
|
try
|
|
{
|
|
controle.BeginInvoke(new Action(async () =>
|
|
{
|
|
try
|
|
{
|
|
await acao();
|
|
if (!tcs.Task.IsCompleted) // Evitar race conditions
|
|
tcs.TrySetResult(null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!tcs.Task.IsCompleted) // Evitar race conditions
|
|
tcs.TrySetException(ex);
|
|
}
|
|
}));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!tcs.Task.IsCompleted) // Garante que o estado do TCS é atualizado corretamente
|
|
tcs.TrySetException(ex);
|
|
}
|
|
|
|
await tcs.Task; // Aguarda a conclusão da ação
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
await acao();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log de exceção opcional, se necessário
|
|
Console.WriteLine($"Erro ao executar ação no thread UI: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public static T ExecutarMetodoComVerificacaoCrossThread<T>(Control controle, Func<T> acao)
|
|
{
|
|
if (controle == null)
|
|
throw new ArgumentNullException(nameof(controle));
|
|
|
|
try
|
|
{
|
|
if (controle.InvokeRequired)
|
|
{
|
|
return (T)controle.Invoke(new Func<T>(() => acao()));
|
|
}
|
|
else
|
|
{
|
|
return acao();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log ou tratamento da exceção
|
|
Console.WriteLine($"Erro em ExecutarMetodoComVerificacaoCrossThread: {ex.Message}");
|
|
throw; // Relançar a exceção para quem chamou lidar com ela, se necessário
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string MontarStringPerformance(float? gpuLoad, float? cpuLoad, float? ramUsage, float? cpuTemp, float? gpuTemp, float? hddLoad)
|
|
{
|
|
return $"HDD: {hddLoad?.ToString("0.0") ?? "N/A"}% " +
|
|
$"GPU: {gpuLoad?.ToString("0.0") ?? "N/A"}% " +
|
|
$"CPU: {cpuLoad?.ToString("0.0") ?? "N/A"}% " +
|
|
$"RAM: {ramUsage?.ToString("0.0") ?? "N/A"}% " +
|
|
$"Temp CPU: {cpuTemp?.ToString("0.0") ?? "N/A"}°C " +
|
|
$"Temp GPU: {gpuTemp?.ToString("0.0") ?? "N/A"}°C";
|
|
}
|
|
|
|
|
|
|
|
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
|
|
|
|
public static async Task SafeExecuteAsync(Func<Task> action)
|
|
{
|
|
await semaphore.WaitAsync();
|
|
try
|
|
{
|
|
await action();
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
}
|
|
|
|
public static void ValidarImagemCameraBrowser(WebView2 nav, Control parent)
|
|
{
|
|
nav.NavigationCompleted += async (sender, args) =>
|
|
{
|
|
if (args.IsSuccess)
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(500); // aguarda carregar conteúdo
|
|
|
|
var browser = (WebView2)sender;
|
|
|
|
string script = "document.body ? document.body.innerText : ''";
|
|
string result = await browser.CoreWebView2.ExecuteScriptAsync(script);
|
|
|
|
// O resultado vem como string JSON (ex: "\"Texto dentro do body\"")
|
|
string conteudo = System.Text.Json.JsonSerializer.Deserialize<string>(result);
|
|
|
|
if (string.IsNullOrWhiteSpace(conteudo) ||
|
|
conteudo.Contains("Internal Server Error") ||
|
|
conteudo.Contains("Not Found") ||
|
|
conteudo.Length < 20)
|
|
{
|
|
Console.WriteLine("Falha no carregamento do stream, recarregando...");
|
|
|
|
await Task.Delay(1000);
|
|
|
|
if (parent.IsHandleCreated)
|
|
{
|
|
parent.Invoke(new Action(() => browser.Reload()));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Câmera carregada com sucesso.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Erro ao verificar a página: {ex.Message}");
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
public static string ConverterComandoBytesParaTexto(byte[] data)
|
|
{
|
|
string comando = string.Join(" ", data.Select(x => x.ToString("X")));
|
|
|
|
return comando;
|
|
}
|
|
|
|
public static byte StringToHexByte(string _st)
|
|
{
|
|
return Convert.ToByte(_st.Replace("0x", ""), 16);
|
|
}
|
|
|
|
public static string HexByteToString(byte _bt)
|
|
{
|
|
return "0x" + _bt.ToString("X").PadLeft(2, '0');
|
|
}
|
|
|
|
public static Bitmap Base64ToBitmap(string base64String)
|
|
{
|
|
if (string.IsNullOrEmpty(base64String))
|
|
return null;
|
|
|
|
byte[] imageBytes = Convert.FromBase64String(base64String);
|
|
using (var ms = new MemoryStream(imageBytes))
|
|
{
|
|
return new Bitmap(ms);
|
|
}
|
|
}
|
|
|
|
public static DateTime UnixToDateTime(string unixTime)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(unixTime))
|
|
return DateTime.MinValue;
|
|
|
|
if (!decimal.TryParse(unixTime, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out decimal timestamp))
|
|
{
|
|
return DateTime.MinValue;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Unix em segundos, inclusive com fração:
|
|
// 1785431515.93917
|
|
if (timestamp >= -62135596800m && timestamp <= 253402300799.999m)
|
|
{
|
|
long milliseconds = decimal.ToInt64(decimal.Truncate(timestamp * 1000m));
|
|
|
|
return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).LocalDateTime;
|
|
}
|
|
|
|
// Unix em milissegundos:
|
|
// 1785431515939
|
|
if (timestamp >= -62135596800000m && timestamp <= 253402300799999m)
|
|
{
|
|
long milliseconds = decimal.ToInt64(decimal.Truncate(timestamp));
|
|
|
|
return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).LocalDateTime;
|
|
}
|
|
|
|
return DateTime.MinValue;
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
return DateTime.MinValue;
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
return DateTime.MinValue;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public static class FuncoesMatematicas
|
|
{
|
|
public static double Map(double valor, double deMin, double deMax, double paraMin, double paraMax)
|
|
{
|
|
return paraMin + (valor - deMin) * (paraMax - paraMin) / (deMax - deMin);
|
|
}
|
|
|
|
public static double ConvertDegreesToRadians(double degrees)
|
|
{
|
|
double radians = (Math.PI / 180) * degrees;
|
|
return (radians);
|
|
}
|
|
|
|
public static double CalcularAlturaTriangulo(double a, double b, double c)
|
|
{
|
|
// Verifica se os lados formam um triângulo válido
|
|
if (a + b <= c || a + c <= b || b + c <= a)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Calcule a área do triângulo usando a fórmula de Herão
|
|
double s = (a + b + c) / 2.0; // Semiperímetro
|
|
double area = Math.Sqrt(s * (s - a) * (s - b) * (s - c));
|
|
|
|
// Calcule a altura em relação à base c
|
|
double altura = (2 * area) / c;
|
|
|
|
return altura;
|
|
}
|
|
|
|
public static double CalcularMediana(List<double> numbers)
|
|
{
|
|
numbers.Sort();
|
|
int count = numbers.Count;
|
|
if (count % 2 == 0)
|
|
{
|
|
// Se count é par, a mediana é a média dos dois elementos do meio
|
|
double middle1 = numbers[count / 2 - 1];
|
|
double middle2 = numbers[count / 2];
|
|
return (middle1 + middle2) / 2.0;
|
|
}
|
|
else
|
|
{
|
|
// Se count é ímpar, a mediana é o elemento do meio
|
|
return numbers[count / 2];
|
|
}
|
|
}
|
|
|
|
public static double CalcularModa(List<double> numbers)
|
|
{
|
|
return numbers.GroupBy(n => n)
|
|
.OrderByDescending(g => g.Count())
|
|
.First()
|
|
.Key;
|
|
}
|
|
|
|
public static double CalcularTemperatura(double leitura, double offset = 0.0)
|
|
{
|
|
double Vs = 5.0; // Tensão de referência do divisor de tensão
|
|
double Beta = 3950.0; // Coeficiente Beta do NTC
|
|
double To = 298.15; // Temperatura em Kelvin correspondente a 25°C
|
|
double Ro = 10000.0; // Resistência do NTC a 25°C
|
|
double adcMax = 4095.0; // Valor máximo do ADC (12 bits ESP32)
|
|
|
|
if (leitura < 200) return -1;
|
|
|
|
// Calcula a tensão de saída do divisor
|
|
double Vout = leitura * Vs / adcMax;
|
|
|
|
// ⚠️ Evita erros matemáticos para valores extremos ⚠️
|
|
if (Vout <= 0.001) return -100.0; // Se tensão for quase zero, temperatura muito baixa
|
|
if (Vout >= Vs) return double.NaN; // Se Vout for igual a Vs, leitura inválida
|
|
|
|
// Calcula a resistência do NTC
|
|
double Rt = Ro * (Vout / (Vs - Vout)); // NTC EM CIMA
|
|
//double Rt = Ro * ((Vs - Vout) / Vout); // NTC EM BAIXO
|
|
|
|
// Aplica a equação de Steinhart-Hart para calcular a temperatura em Kelvin
|
|
double T = 1.0 / (1.0 / To + Math.Log(Rt / Ro) / Beta);
|
|
|
|
// Converte Kelvin para Celsius e aplica offset
|
|
double Tc = T - 273.15 + offset;
|
|
|
|
return Math.Round(Tc, 2);
|
|
}
|
|
|
|
public static bool ValorEstaEntre(double valorAtual, double valorComparar, double margem)
|
|
{
|
|
if (valorAtual <= (valorComparar + margem) && valorAtual >= (valorComparar - margem))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Função personalizada Clamp
|
|
public static double Clamp(double valor, double minimo, double maximo)
|
|
{
|
|
if (valor < minimo) return minimo;
|
|
if (valor > maximo) return maximo;
|
|
return valor;
|
|
}
|
|
|
|
public static double CalculaVelocidadeRPM(double RPM)
|
|
{
|
|
double VelocidadeInstantanea = Math.Round((Math.PI * VariaveisEquipamento.DiametroRoda * 60.0 * RPM) / 1000.0, 2); // Km/h
|
|
return VelocidadeInstantanea;
|
|
}
|
|
|
|
public static double CalculaRPMVelocidade(double Velocidade)
|
|
{
|
|
double RPM = Math.Round((Velocidade * 1000) / (Math.PI * VariaveisEquipamento.DiametroRoda * 60), 2);
|
|
return RPM;
|
|
}
|
|
|
|
public static double ConverteKmhParaMs(double Velocidade)
|
|
{
|
|
return Velocidade / 3.6;
|
|
}
|
|
|
|
public static double ConverteMsParaKmh(double Velocidade)
|
|
{
|
|
return Velocidade * 3.6;
|
|
}
|
|
|
|
public static double CalculaVelocidadeMsPercentual(double percentual)
|
|
{
|
|
double Velocidade = ConverteKmhParaMs(CalculaVelocidadeRPM((percentual / 100.0) * VariaveisEquipamento.RPM_Max_Roda));
|
|
return Velocidade;
|
|
}
|
|
|
|
public static double CalculaVelocidadePercentualMs(double vel_ms)
|
|
{
|
|
double Velocidade = vel_ms / CalculaVelocidadeMsPercentual(100);
|
|
return Velocidade;
|
|
}
|
|
|
|
public static double GrausParaRadianos(double angle)
|
|
{
|
|
return Math.PI / 180.0 * angle;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
public static class CloneHelper
|
|
{
|
|
public static T DeepClone<T>(T origem)
|
|
{
|
|
if (object.ReferenceEquals(origem, null))
|
|
return default(T);
|
|
|
|
JsonSerializerSettings settings =
|
|
new JsonSerializerSettings
|
|
{
|
|
ObjectCreationHandling = ObjectCreationHandling.Replace,
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
|
};
|
|
|
|
string json = JsonConvert.SerializeObject(origem, settings);
|
|
|
|
return JsonConvert.DeserializeObject<T>(json, settings);
|
|
}
|
|
}
|
|
}
|