agrobot_base/AgroBase/AgroBase/Models/Variaveis.cs

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