2025-11-14 17:11:58 +00:00
|
|
|
|
using AgroBase.Models;
|
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using OperationControl.Services;
|
2025-11-24 16:05:50 +00:00
|
|
|
|
using AgroMonitor;
|
|
|
|
|
|
using Application = System.Windows.Application;
|
|
|
|
|
|
using AgroBase.Models.Operacoes;
|
2025-11-14 17:11:58 +00:00
|
|
|
|
|
|
|
|
|
|
namespace OperationControl.Models
|
|
|
|
|
|
{
|
|
|
|
|
|
public class Variaveis
|
|
|
|
|
|
{
|
|
|
|
|
|
public static MqttService MqttService;
|
|
|
|
|
|
public static GpsService GpsService;
|
|
|
|
|
|
|
|
|
|
|
|
public static void MostrarLog(string message)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.WriteLine(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static async void IniciarMQTT()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (MqttService != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var topico in MqttService.Topicos.Where(x => x.Inscrever))
|
|
|
|
|
|
{
|
|
|
|
|
|
await MqttService.UnsubscribeAsync(topico);
|
|
|
|
|
|
}
|
|
|
|
|
|
MqttService.Topicos.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
MqttService = new MqttService("localhost", 1883, "base", true, msg => MostrarLog($"[MQTT localhost:1883] - {msg}"));
|
|
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
await MqttService.AdicionarNovoTopico(VariaveisMonitoramento.TopicoMqttPosicao);
|
|
|
|
|
|
await MqttService.AdicionarNovoTopico(VariaveisMonitoramento.TopicoMqttRTCM);
|
|
|
|
|
|
await MqttService.AdicionarNovoTopico(VariaveisMonitoramento.TopicoMqttDispositivos, true, 1, async (message) =>
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
string device_id = message.Mensagem;
|
|
|
|
|
|
VariaveisControleOperacao.AdicionarNovoRoverNaRede(message.Mensagem);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
MostrarLog($"Erro ao deserializar ping do rover: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
GpsService = new GpsService();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class VariaveisControleOperacao
|
|
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
public static double LeverArmFrontal { get; } = 0.0; // cm
|
|
|
|
|
|
public static double LeverArmLateral { get; } = 0.0; // cm
|
2025-11-14 17:11:58 +00:00
|
|
|
|
|
|
|
|
|
|
public static readonly string BaseMarkerID = "BASE";
|
|
|
|
|
|
private static readonly object _RoversLock = new object();
|
2025-11-24 16:05:50 +00:00
|
|
|
|
private static double TempoRoverVivo = 5.0;
|
|
|
|
|
|
private static bool AdicionandoNovoRover = false;
|
|
|
|
|
|
private static bool AtualizandoListaRovers = false;
|
|
|
|
|
|
public static List<OperacaoParametrosModel> RoversNaRede { get; set; } = new List<OperacaoParametrosModel>();
|
2025-11-14 17:11:58 +00:00
|
|
|
|
public static string SelectedRoverId { get; set; } = BaseMarkerID;
|
2025-11-24 16:05:50 +00:00
|
|
|
|
public static OperacaoParametrosModel? RoverEmFoco => RoversNaRede.FirstOrDefault(x => x.RoverId == SelectedRoverId);
|
|
|
|
|
|
public static bool BaseEmFoco => SelectedRoverId == BaseMarkerID;
|
2025-11-14 17:11:58 +00:00
|
|
|
|
|
|
|
|
|
|
public static async void AdicionarNovoRoverNaRede(string device_id)
|
|
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
if (AdicionandoNovoRover) return;
|
|
|
|
|
|
AdicionandoNovoRover = true;
|
2025-11-14 17:11:58 +00:00
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
try
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
if (string.IsNullOrEmpty(device_id))
|
|
|
|
|
|
return;
|
2025-11-14 17:11:58 +00:00
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
bool rover_ja_adicionado = false;
|
2025-11-14 17:11:58 +00:00
|
|
|
|
lock (_RoversLock)
|
|
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
rover_ja_adicionado = RoversNaRede.Any(x => x.RoverId == device_id);
|
2025-11-14 17:11:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
if (!rover_ja_adicionado)
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
lock (_RoversLock)
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
RoversNaRede.Add(new OperacaoParametrosModel()
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
RoverId = device_id,
|
|
|
|
|
|
Configurado = false,
|
|
|
|
|
|
Alive = true,
|
|
|
|
|
|
DadosLeitura = new OperacaoSensoriamentoLogModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-14 17:11:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
bool t_hbt_added = Variaveis.MqttService.Topicos.Any(x => x.Topico == VariaveisEquipamento.TopicoMqttHeartbeat.Replace("<id>", device_id));
|
|
|
|
|
|
if (!t_hbt_added)
|
|
|
|
|
|
await Variaveis.MqttService.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttHeartbeat.Replace("<id>", device_id));
|
|
|
|
|
|
bool t_cmd_added = Variaveis.MqttService.Topicos.Any(x => x.Topico == VariaveisEquipamento.TopicoMqttComandos.Replace("<id>", device_id));
|
|
|
|
|
|
if (!t_cmd_added)
|
|
|
|
|
|
await Variaveis.MqttService.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttComandos.Replace("<id>", device_id));
|
|
|
|
|
|
bool t_tel_added = Variaveis.MqttService.Topicos.Any(x => x.Topico == VariaveisEquipamento.TopicoMqttTelemetria.Replace("<id>", device_id));
|
|
|
|
|
|
if (!t_tel_added)
|
|
|
|
|
|
await Variaveis.MqttService.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttTelemetria.Replace("<id>", device_id), true, 1, async (message) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
string json = message.Mensagem;
|
|
|
|
|
|
if (string.IsNullOrEmpty(json)) return;
|
|
|
|
|
|
var obj = JsonConvert.DeserializeObject<OperacaoSensoriamentoLogModel>(json);
|
2025-11-25 20:14:31 +00:00
|
|
|
|
if (obj == null) return;
|
2025-11-24 16:05:50 +00:00
|
|
|
|
lock (_RoversLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rover = RoversNaRede.FirstOrDefault(x => x.RoverId == device_id);
|
|
|
|
|
|
if (rover != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
obj.Momento = DateTime.Now;
|
|
|
|
|
|
rover.DadosLeitura = obj;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
((App)Application.Current).Shell.Main?.AtualizarDadosTela_Telemetria(device_id);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Variaveis.MostrarLog($"Erro ao deserializar dados de telemetria do rover {device_id}: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
bool t_par_added = Variaveis.MqttService.Topicos.Any(x => x.Topico == VariaveisEquipamento.TopicoMqttParametros.Replace("<id>", device_id));
|
|
|
|
|
|
if (!t_par_added)
|
|
|
|
|
|
await Variaveis.MqttService.AdicionarNovoTopico(VariaveisEquipamento.TopicoMqttParametros.Replace("<id>", device_id), true, 1, async (message) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
string json = message.Mensagem;
|
|
|
|
|
|
if (string.IsNullOrEmpty(json)) return;
|
|
|
|
|
|
var obj = JsonConvert.DeserializeObject<OperacaoParametrosModel>(json);
|
|
|
|
|
|
lock (_RoversLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rover = RoversNaRede.FirstOrDefault(x => x.RoverId == device_id);
|
|
|
|
|
|
if (rover != null)
|
|
|
|
|
|
{
|
2025-11-25 12:17:19 +00:00
|
|
|
|
RoversNaRede.Remove(rover);
|
|
|
|
|
|
RoversNaRede.Add(obj);
|
2025-11-24 16:05:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-24 19:58:20 +00:00
|
|
|
|
((App)Application.Current).Shell.Main?.AtualizarDadosTela_ParametrosOperacao(obj.Controle);
|
2025-11-24 16:05:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Variaveis.MostrarLog($"Erro ao deserializar dados de parametros do rover {device_id}: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
List<string> dispositivos_ids = new List<string>() { "BASE" };
|
|
|
|
|
|
lock (_RoversLock)
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
dispositivos_ids = dispositivos_ids.Union(RoversNaRede.Select(x => x.RoverId).ToList()).ToList();
|
2025-11-14 17:11:58 +00:00
|
|
|
|
}
|
2025-11-24 16:05:50 +00:00
|
|
|
|
((App)Application.Current).Shell.Main?.AtualizarListaDispositivos(dispositivos_ids);
|
2025-11-24 19:58:20 +00:00
|
|
|
|
|
|
|
|
|
|
RequisitarParametrosOperacao();
|
2025-11-24 16:05:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
AdicionandoNovoRover = false;
|
2025-11-14 17:11:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static async void AtualizarListaRoversNaRede()
|
|
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
if (AtualizandoListaRovers) return;
|
|
|
|
|
|
AtualizandoListaRovers = true;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
var rovers_atual = new List<OperacaoParametrosModel>();
|
|
|
|
|
|
lock (_RoversLock)
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
rovers_atual = new List<OperacaoParametrosModel>(RoversNaRede);
|
2025-11-14 17:11:58 +00:00
|
|
|
|
}
|
2025-11-24 16:05:50 +00:00
|
|
|
|
foreach (var rover in rovers_atual)
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
rover.Alive = (DateTime.Now - rover.DadosLeitura.Momento).TotalSeconds < TempoRoverVivo;
|
|
|
|
|
|
if (!rover.Alive)
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
((App)Application.Current).Shell.Main?.MAP?.markers?.UpdateMarkerInfo(rover.RoverId, status: AgroBase.Models.Enums.StatusOperacao.Erro);
|
2025-11-14 17:11:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-24 16:05:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
AtualizandoListaRovers = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static async void EnviarDadosHeartbeat()
|
|
|
|
|
|
{
|
|
|
|
|
|
var topicos = Variaveis.MqttService.Topicos.Where(x => x.Topico.Contains(VariaveisEquipamento.TopicoMqttHeartbeat.Split("/")[VariaveisEquipamento.TopicoMqttHeartbeat.Split("/").Length - 1]));
|
|
|
|
|
|
foreach (var topico in topicos)
|
|
|
|
|
|
{
|
|
|
|
|
|
await Variaveis.MqttService.PublishAsync(topico, "0");
|
2025-11-14 17:11:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
private static async void EnviarDadosControle(string rover_id, OperacaoComandoBaseModel controle)
|
2025-11-14 17:11:58 +00:00
|
|
|
|
{
|
|
|
|
|
|
var topico = Variaveis.MqttService.Topicos.FirstOrDefault(x => x.Topico == VariaveisEquipamento.TopicoMqttComandos.Replace("<id>", rover_id));
|
|
|
|
|
|
if (topico != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
string json = JsonConvert.SerializeObject(controle);
|
|
|
|
|
|
await Variaveis.MqttService.PublishAsync(topico, json);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static async void EnviarDadosPosicao(GPSModel posicao)
|
|
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
var topico = Variaveis.MqttService.Topicos.FirstOrDefault(x => x.Topico == VariaveisMonitoramento.TopicoMqttPosicao);
|
2025-11-14 17:11:58 +00:00
|
|
|
|
if (topico != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
string json = JsonConvert.SerializeObject(posicao);
|
|
|
|
|
|
await Variaveis.MqttService.PublishAsync(topico, json);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static async void EnviarDadosCorrecaoRTCM(byte[] correcao)
|
|
|
|
|
|
{
|
2025-11-24 16:05:50 +00:00
|
|
|
|
var topico = Variaveis.MqttService.Topicos.FirstOrDefault(x => x.Topico == VariaveisMonitoramento.TopicoMqttRTCM);
|
2025-11-14 17:11:58 +00:00
|
|
|
|
if (topico != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await Variaveis.MqttService.PublishAsync(topico, correcao);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
#region Controle
|
|
|
|
|
|
|
|
|
|
|
|
public static void EnviarComandoIniciarOperacao(bool iniciar, bool simulador = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now,
|
|
|
|
|
|
Dispositivo = AgroBase.Models.Enums.T_Code.Mod,
|
2025-11-25 12:17:19 +00:00
|
|
|
|
Tecla = simulador ? AgroBase.Models.Enums.BotoesJoystick.L1 : iniciar ? AgroBase.Models.Enums.BotoesJoystick.L2 : AgroBase.Models.Enums.BotoesJoystick.L3
|
2025-11-24 16:05:50 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code disp, bool ligar)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now,
|
|
|
|
|
|
Dispositivo = disp,
|
|
|
|
|
|
Tecla = ligar ? AgroBase.Models.Enums.BotoesJoystick.L1 : AgroBase.Models.Enums.BotoesJoystick.Vazio
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void EnviarComandoParada(bool? emergencia = null, bool? pausa = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cmd = new OperacaoComandoBaseModel() { Momento = DateTime.Now };
|
|
|
|
|
|
if (emergencia != null) cmd.Emergencia = (bool)emergencia;
|
|
|
|
|
|
if (pausa != null) cmd.Pausa = (bool)pausa;
|
|
|
|
|
|
if (emergencia != null || pausa != null)
|
|
|
|
|
|
EnviarDadosControle(SelectedRoverId, cmd);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void EnviarComandoReferenciamento()
|
|
|
|
|
|
{
|
|
|
|
|
|
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now,
|
|
|
|
|
|
Tecla = AgroBase.Models.Enums.BotoesJoystick.R3
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void EnviarComandoLeverArm(double valorCm)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now,
|
|
|
|
|
|
Dispositivo = AgroBase.Models.Enums.T_Code.Gps,
|
|
|
|
|
|
LeverArmLateral = Math.Round(valorCm, 0),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void EnviarParametrosOperacao(OperacaoParametrosModel parametros)
|
|
|
|
|
|
{
|
2025-11-25 20:14:31 +00:00
|
|
|
|
bool operacao_iniciada = (RoverEmFoco?.DadosLeitura?.OperacaoIniciada ?? false);
|
|
|
|
|
|
bool parametros_parciais = parametros.Modo == null;
|
|
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now,
|
2025-11-25 12:17:19 +00:00
|
|
|
|
Dispositivo = AgroBase.Models.Enums.T_Code.Mod,
|
2025-11-24 16:05:50 +00:00
|
|
|
|
Parametros = parametros,
|
2025-11-25 20:14:31 +00:00
|
|
|
|
Tecla = (operacao_iniciada || parametros_parciais ? AgroBase.Models.Enums.BotoesJoystick.Touchpad : AgroBase.Models.Enums.BotoesJoystick.Options)
|
2025-11-24 16:05:50 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 19:58:20 +00:00
|
|
|
|
public static void RequisitarParametrosOperacao()
|
|
|
|
|
|
{
|
|
|
|
|
|
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now,
|
2025-11-25 12:17:19 +00:00
|
|
|
|
Dispositivo = AgroBase.Models.Enums.T_Code.Mod,
|
2025-11-24 19:58:20 +00:00
|
|
|
|
Tecla = AgroBase.Models.Enums.BotoesJoystick.Share
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 20:14:31 +00:00
|
|
|
|
public static void EnviarComandoAtuador(string componente_id, bool status)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now,
|
|
|
|
|
|
Dispositivo = AgroBase.Models.Enums.T_Code.Atu,
|
|
|
|
|
|
_comp_id = componente_id,
|
|
|
|
|
|
_comp_value = status
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void EnviarComandoSensoriamento(string componente_id, dynamic status)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
Momento = DateTime.Now,
|
|
|
|
|
|
Dispositivo = AgroBase.Models.Enums.T_Code.Sen,
|
|
|
|
|
|
_comp_id = componente_id,
|
|
|
|
|
|
_comp_value = status
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:05:50 +00:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
2025-11-14 17:11:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|