Compare commits
No commits in common. "b737b11a592ed0de5867d3a61388ba235e0d3eef" and "00c9af952a2ae81b253e620ce892138fd6048271" have entirely different histories.
b737b11a59
...
00c9af952a
|
|
@ -1,4 +1,4 @@
|
|||
using AgroBase.Models;
|
||||
using AgroBase.Models;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
|
|
@ -52,8 +52,6 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
|
||||
private void CarregarDadosTela()
|
||||
{
|
||||
_carregandoDados = true;
|
||||
|
||||
try
|
||||
{
|
||||
var parametros = op?.Parametros?.Controle;
|
||||
|
|
@ -99,12 +97,9 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
|
||||
parametros.AtuAgitadorModo = seletorModoAgitador.SelectedMode;
|
||||
|
||||
if (agitador != null)
|
||||
{
|
||||
agitador.Kp = (double)paramKp.Value;
|
||||
agitador.Ki = (double)paramKi.Value;
|
||||
agitador.Kd = (double)paramKd.Value;
|
||||
}
|
||||
agitador.Kp = (double)paramKp.Value;
|
||||
agitador.Ki = (double)paramKi.Value;
|
||||
agitador.Kd = (double)paramKd.Value;
|
||||
|
||||
parametros.AtuDuracaoAtuacao = (int)paramDuracaoAtuacao.Value;
|
||||
parametros.AtuPressaoLinha = (double)paramPressaoLinha.Value;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AgroBase.Forms.IHM.Controls;
|
||||
using AgroBase.Forms.IHM.Controls;
|
||||
using AgroBase.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -14,7 +14,6 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
|
||||
private bool _carregandoDados;
|
||||
private bool _mapaExistenteCarregado;
|
||||
private bool _hidratandoMapaExistente;
|
||||
|
||||
public ucParametrosMapa()
|
||||
{
|
||||
|
|
@ -57,12 +56,8 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
if (mesmaInstanciaMapa && !recarregarMapa)
|
||||
{
|
||||
_mapaExistenteCarregado = true;
|
||||
lblMapaPlaceholder.Visible =
|
||||
(_mapa.mapaService?.DadosMapa ?? op.Parametros?.Mapa) == null;
|
||||
|
||||
// Ao apenas reanexar a mesma instância, nunca sobrescrevemos
|
||||
// Parametros.RuasPercorrer com uma lista runtime vazia.
|
||||
SincronizarParametrosComMapa(permitirSelecaoVazia: false);
|
||||
lblMapaPlaceholder.Visible = _mapa.mapaService?.DadosMapa == null;
|
||||
SincronizarParametrosComMapa();
|
||||
AtualizarResumo();
|
||||
return;
|
||||
}
|
||||
|
|
@ -77,28 +72,17 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
|
||||
try
|
||||
{
|
||||
MapaFeatureCollectionModel mapaRuntime =
|
||||
_mapa?.mapaService?.DadosMapa;
|
||||
lblMapaPlaceholder.Visible = _mapa?.mapaService?.DadosMapa == null;
|
||||
|
||||
lblMapaPlaceholder.Visible =
|
||||
(mapaRuntime ?? op?.Parametros?.Mapa) == null;
|
||||
SincronizarParametrosComMapa();
|
||||
AtualizarResumo();
|
||||
|
||||
/*
|
||||
* Primeiro preservamos o snapshot da operação. Se o MapasModel
|
||||
* runtime ainda não foi hidratado, copiar dele agora apagaria
|
||||
* Mapa/RuasPercorrer dos parâmetros.
|
||||
*/
|
||||
if (!_mapaExistenteCarregado && op?.Parametros?.Mapa != null)
|
||||
{
|
||||
_mapaExistenteCarregado = true;
|
||||
AtualizarResumo();
|
||||
|
||||
CarregarMapaExistenteAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
SincronizarParametrosComMapa(permitirSelecaoVazia: false);
|
||||
AtualizarResumo();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -108,57 +92,28 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
|
||||
private async void CarregarMapaExistenteAsync()
|
||||
{
|
||||
if (_hidratandoMapaExistente)
|
||||
return;
|
||||
|
||||
_hidratandoMapaExistente = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (_mapa == null || op?.Parametros?.Mapa == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Snapshot antes do primeiro await. Eventos do mapa podem ocorrer
|
||||
* durante CarregarMapaAsync; eles não podem apagar esta seleção.
|
||||
*/
|
||||
List<string> ruasSelecionadas =
|
||||
op.Parametros.RuasPercorrer != null
|
||||
? new List<string>(op.Parametros.RuasPercorrer)
|
||||
: new List<string>();
|
||||
List<string> ruasSelecionadas = op.Parametros.RuasPercorrer != null ? new List<string>(op.Parametros.RuasPercorrer) : new List<string>();
|
||||
|
||||
await _mapa.CarregarMapaAsync(
|
||||
op.Parametros.Mapa,
|
||||
"Mapa da operação"
|
||||
);
|
||||
await _mapa.CarregarMapaAsync(op.Parametros.Mapa, "Mapa da operação");
|
||||
|
||||
_mapa.TipoMapa = op.Parametros.TipoMapa;
|
||||
|
||||
await _mapa.DefinirRuasSelecionadasAsync(
|
||||
op,
|
||||
ruasSelecionadas,
|
||||
false
|
||||
);
|
||||
|
||||
/*
|
||||
* Agora o runtime está hidratado. Sincronizamos uma única vez,
|
||||
* inclusive aceitando lista vazia quando ela realmente veio dos
|
||||
* parâmetros da operação.
|
||||
*/
|
||||
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
|
||||
await _mapa.DefinirRuasSelecionadasAsync(op, ruasSelecionadas, false);
|
||||
|
||||
lblMapaPlaceholder.Visible = false;
|
||||
|
||||
AtualizarResumo();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Variaveis.MostrarLog(
|
||||
"[ucParametrosMapa.CarregarMapaExistente] " + ex.Message
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_hidratandoMapaExistente = false;
|
||||
Variaveis.MostrarLog("[ucParametrosMapa.CarregarMapaExistente] " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,9 +136,6 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
if (IsDisposed || Disposing)
|
||||
return;
|
||||
|
||||
if (_hidratandoMapaExistente)
|
||||
return;
|
||||
|
||||
if (InvokeRequired)
|
||||
{
|
||||
BeginInvoke(new Action(
|
||||
|
|
@ -198,13 +150,13 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
|
||||
lblMapaPlaceholder.Visible = false;
|
||||
|
||||
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
|
||||
SincronizarParametrosComMapa();
|
||||
AtualizarResumo();
|
||||
}
|
||||
|
||||
private void Mapa_RuasSelecionadasAlteradas(object sender, EventArgs e)
|
||||
{
|
||||
if (_carregandoDados || _hidratandoMapaExistente)
|
||||
if (_carregandoDados)
|
||||
return;
|
||||
|
||||
if (IsDisposed || Disposing)
|
||||
|
|
@ -222,52 +174,27 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
|||
return;
|
||||
}
|
||||
|
||||
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
|
||||
SincronizarParametrosComMapa();
|
||||
AtualizarResumo();
|
||||
}
|
||||
|
||||
private void SincronizarParametrosComMapa(bool permitirSelecaoVazia)
|
||||
private void SincronizarParametrosComMapa()
|
||||
{
|
||||
if (op?.Parametros == null || _mapa == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MapaFeatureCollectionModel dadosMapa =
|
||||
_mapa.mapaService?.DadosMapa;
|
||||
|
||||
/*
|
||||
* A tela de parâmetros não é a fonte primária do mapa durante uma
|
||||
* operação em andamento. Se o WebView/MapasModel ainda não carregou,
|
||||
* mantemos intacto o snapshot que já veio em op.Parametros.
|
||||
*/
|
||||
if (dadosMapa == null)
|
||||
return;
|
||||
|
||||
op.Parametros.Mapa = dadosMapa;
|
||||
op.Parametros.Mapa = _mapa.mapaService?.DadosMapa;
|
||||
op.Parametros.TipoMapa = _mapa.TipoMapa;
|
||||
|
||||
List<string> ruasRuntime =
|
||||
_mapa.RuasPercorrer != null
|
||||
? _mapa.RuasPercorrer.ToList()
|
||||
: new List<string>();
|
||||
|
||||
if (permitirSelecaoVazia || ruasRuntime.Count > 0)
|
||||
op.Parametros.RuasPercorrer = ruasRuntime;
|
||||
op.Parametros.RuasPercorrer = _mapa.RuasPercorrer != null ? _mapa.RuasPercorrer.ToList() : new List<string>();
|
||||
}
|
||||
|
||||
private void AtualizarResumo()
|
||||
{
|
||||
MapaFeatureCollectionModel dadosMapa =
|
||||
_mapa?.mapaService?.DadosMapa ??
|
||||
op?.Parametros?.Mapa;
|
||||
|
||||
List<MapaFeatureModel> features =
|
||||
dadosMapa?.features ??
|
||||
new List<MapaFeatureModel>();
|
||||
|
||||
List<string> selecionadas =
|
||||
(_mapa?.RuasPercorrer?.Count ?? 0) > 0
|
||||
? _mapa.RuasPercorrer
|
||||
: (op?.Parametros?.RuasPercorrer ?? new List<string>());
|
||||
MapaFeatureCollectionModel dadosMapa = _mapa?.mapaService?.DadosMapa;
|
||||
List<MapaFeatureModel> features = dadosMapa?.features ?? new List<MapaFeatureModel>();
|
||||
List<string> selecionadas = _mapa?.RuasPercorrer ?? new List<string>();
|
||||
int quantidadeTotal = features.Count;
|
||||
int quantidadeSelecionada = selecionadas.Count;
|
||||
double distanciaTotalMapa = features
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AgroBase.Forms.IHM.Controls;
|
||||
using AgroBase.Forms.IHM.Controls;
|
||||
using AgroBase.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -193,8 +193,6 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
pnlAlertaAutonomia.Visible = false;
|
||||
pnlStatusAutonomia.Visible = true;
|
||||
|
||||
btnLiberarSupervisao.Enabled = false;
|
||||
|
||||
pnlStatusAutonomia.BringToFront();
|
||||
}
|
||||
|
||||
|
|
@ -206,36 +204,6 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
pnlAlertaAutonomia.BringToFront();
|
||||
}
|
||||
|
||||
private static bool PodeLiberarSupervisao(AutonomiaCorredorModel autonomia)
|
||||
{
|
||||
if (autonomia == null || autonomia.Liberado)
|
||||
return false;
|
||||
|
||||
bool bateriaBloqueando =
|
||||
autonomia.DadosBateriaValidos &&
|
||||
!autonomia.BateriaSuficiente &&
|
||||
!autonomia.BateriaOverrideAplicado;
|
||||
|
||||
bool herbicidaBloqueando =
|
||||
autonomia.PulverizacaoExigida &&
|
||||
autonomia.DadosHerbicidaValidos &&
|
||||
!autonomia.HerbicidaSuficiente &&
|
||||
!autonomia.HerbicidaOverrideAplicado;
|
||||
|
||||
/*
|
||||
* AguardandoLiberacaoManual representa uma trava persistente que
|
||||
* precisa de reconhecimento humano mesmo se o recurso já recuperou.
|
||||
* Critico representa uma insuficiência atual e também deve permitir
|
||||
* a decisão supervisionada, sem obrigar o operador a esperar o latch.
|
||||
*/
|
||||
return
|
||||
autonomia.Status == AutonomiaCorredorStatus.AguardandoLiberacaoManual ||
|
||||
(
|
||||
autonomia.Status == AutonomiaCorredorStatus.Critico &&
|
||||
(bateriaBloqueando || herbicidaBloqueando)
|
||||
);
|
||||
}
|
||||
|
||||
private int ObterIndiceCorredorAvaliado()
|
||||
{
|
||||
int indiceAtual =
|
||||
|
|
@ -259,44 +227,8 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
|
||||
private void btnLiberarSupervisao_Click(object sender, EventArgs e)
|
||||
{
|
||||
var autonomia = op?.Trajetoria?.AutonomiaCorredor;
|
||||
|
||||
if (!PodeLiberarSupervisao(autonomia))
|
||||
return;
|
||||
|
||||
int idxCorredor = ObterIndiceCorredorAvaliado();
|
||||
|
||||
bool bateriaBloqueando =
|
||||
autonomia.DadosBateriaValidos &&
|
||||
!autonomia.BateriaSuficiente &&
|
||||
!autonomia.BateriaOverrideAplicado;
|
||||
|
||||
bool herbicidaBloqueando =
|
||||
autonomia.PulverizacaoExigida &&
|
||||
autonomia.DadosHerbicidaValidos &&
|
||||
!autonomia.HerbicidaSuficiente &&
|
||||
!autonomia.HerbicidaOverrideAplicado;
|
||||
|
||||
bool aguardandoReconhecimento =
|
||||
autonomia.Status == AutonomiaCorredorStatus.AguardandoLiberacaoManual;
|
||||
|
||||
/*
|
||||
* Em AguardandoLiberacaoManual o motivo original da trava pode já
|
||||
* ter se recuperado, e o modelo não expõe publicamente qual latch
|
||||
* ficou pendente. Nesse caso reconhecemos ambos, como a implementação
|
||||
* antiga já fazia.
|
||||
*
|
||||
* Em Critico liberamos apenas o recurso que realmente está bloqueando.
|
||||
*/
|
||||
bool liberarBateria = aguardandoReconhecimento || bateriaBloqueando;
|
||||
bool liberarHerbicida = aguardandoReconhecimento || herbicidaBloqueando;
|
||||
|
||||
op?.Trajetoria?.AtualizarDadosAutonomiaCorredor(
|
||||
idxCorredor: idxCorredor,
|
||||
bat_liberada: liberarBateria,
|
||||
herb_liberado: liberarHerbicida
|
||||
);
|
||||
|
||||
op?.Trajetoria?.AtualizarDadosAutonomiaCorredor(idxCorredor: idxCorredor, bat_liberada: true, herb_liberado: true);
|
||||
AtualizarResultadoAutonomia(op?.Trajetoria?.AutonomiaCorredor);
|
||||
}
|
||||
|
||||
|
|
@ -535,21 +467,13 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
|
||||
bool operacaoIniciada = op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false;
|
||||
|
||||
bool podeLiberarSupervisao = PodeLiberarSupervisao(autonomia);
|
||||
bool aguardandoLiberacao = autonomia.Status == AutonomiaCorredorStatus.AguardandoLiberacaoManual && !autonomia.Liberado;
|
||||
|
||||
btnLiberarSupervisao.Enabled =
|
||||
operacaoIniciada &&
|
||||
podeLiberarSupervisao;
|
||||
|
||||
if (operacaoIniciada && podeLiberarSupervisao)
|
||||
if (operacaoIniciada && aguardandoLiberacao)
|
||||
{
|
||||
AtualizarTextoAlertaAutonomia(autonomia);
|
||||
|
||||
MostrarAlertaAutonomia();
|
||||
|
||||
// MostrarStatusAutonomia() desabilita o botão, então restauramos
|
||||
// explicitamente o estado após trazer o painel de alerta.
|
||||
btnLiberarSupervisao.Enabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -559,13 +483,11 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
private void AtualizarTextoAlertaAutonomia(AutonomiaCorredorModel autonomia)
|
||||
{
|
||||
bool bateriaBloqueando =
|
||||
autonomia.DadosBateriaValidos &&
|
||||
!autonomia.BateriaSuficiente &&
|
||||
!autonomia.BateriaOverrideAplicado;
|
||||
|
||||
bool herbicidaBloqueando =
|
||||
autonomia.PulverizacaoExigida &&
|
||||
autonomia.DadosHerbicidaValidos &&
|
||||
!autonomia.HerbicidaSuficiente &&
|
||||
!autonomia.HerbicidaOverrideAplicado;
|
||||
|
||||
|
|
@ -722,7 +644,10 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
lblStatusAvaliacao.Text = !string.IsNullOrWhiteSpace(autonomia.Motivo) ? autonomia.Motivo : "Autonomia atualizada";
|
||||
lblStatusAvaliacao.ForeColor = autonomia.Liberado ? corCorredor : HmiTheme.TextMuted;
|
||||
|
||||
AtualizarPainelAutonomia(autonomia);
|
||||
if (autonomia.Liberado)
|
||||
MostrarStatusAutonomia();
|
||||
else
|
||||
MostrarAlertaAutonomia();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AgroBase.Forms.IHM.Controls;
|
||||
using AgroBase.Forms.IHM.Controls;
|
||||
using AgroBase.Forms.IHM.Operacao.Parametros;
|
||||
using AgroBase.Models;
|
||||
using System;
|
||||
|
|
@ -155,36 +155,18 @@ namespace AgroBase.Forms.IHM.Operacao
|
|||
if (operacaoParametrizando == null)
|
||||
return;
|
||||
|
||||
var op = Variaveis.OperacaoEmAndamento;
|
||||
bool operacaoIniciada =
|
||||
op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false;
|
||||
|
||||
/*
|
||||
* A exigência de mapa/ruas é válida para parametrizar uma operação
|
||||
* nova. Durante uma operação já iniciada, o botão Salvar realiza
|
||||
* atualização parcial (pulverizador, agitador, velocidades etc.) e
|
||||
* não deve ser bloqueado por uma validação estrutural do mapa.
|
||||
*/
|
||||
if (!operacaoIniciada &&
|
||||
operacaoParametrizando.Parametros.Modo == ModoOperacao.MapaGPS)
|
||||
if (operacaoParametrizando.Parametros.Modo == ModoOperacao.MapaGPS && (operacaoParametrizando.Mapa.RuasPercorrer?.Count ?? 0) == 0)
|
||||
{
|
||||
int qtdRuas =
|
||||
operacaoParametrizando.Parametros?.RuasPercorrer?.Count
|
||||
?? operacaoParametrizando.Mapa?.RuasPercorrer?.Count
|
||||
?? 0;
|
||||
|
||||
if (qtdRuas == 0)
|
||||
{
|
||||
FuncoesIHM.MostrarAlerta(
|
||||
"Trajetória não parametrizada",
|
||||
$"Para salvar os parâmetros da operação {operacaoParametrizando.Sensoriamento.Operacao.Modo}, é necessário configurar o mapa e as ruas a serem percorridas!",
|
||||
MessageBoxIcon.Warning
|
||||
);
|
||||
return;
|
||||
}
|
||||
FuncoesIHM.MostrarAlerta(
|
||||
"Trajetória não parametrizada",
|
||||
$"Para salvar os parâmetros da operação {operacaoParametrizando.Sensoriamento.Operacao.Modo}, é necessário configurar o mapa e as ruas a serem percorridas!",
|
||||
MessageBoxIcon.Warning
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (operacaoIniciada)
|
||||
var op = Variaveis.OperacaoEmAndamento;
|
||||
if (op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false)
|
||||
op?.AtualizarParametrosParciais(operacaoParametrizando.Parametros, op);
|
||||
else
|
||||
OperacaoModel.CarregarParamerosOperacaoBase(operacaoParametrizando.Parametros);
|
||||
|
|
|
|||
|
|
@ -772,7 +772,7 @@ namespace AgroBase.Forms
|
|||
lblDistProx.Text =
|
||||
$"Distância Próximo Ponto: {(proximoPonto?.DistanciaAtual ?? 0):F2} m";
|
||||
lblDistAnt.Text =
|
||||
$"Esq: {dadosTrajetoria.DistanciaEsquerda * 100:F0} cm | Dir: {dadosTrajetoria.DistanciaDireita * 100:F0} cm | Err: {(dadosTrajetoria.DistanciaDireita - dadosTrajetoria.DistanciaEsquerda) * 100:F0} cm";
|
||||
$"Dist Esquerda: {dadosTrajetoria.DistanciaEsquerda * 100:F0} cm | Dist Direita: {dadosTrajetoria.DistanciaDireita * 100:F0} cm";
|
||||
lblTempoRestante.Text =
|
||||
$"Tempo Restante: {dadosTrajetoria.TempoEstimadoRestante}";
|
||||
lblTempoOperacao.Text =
|
||||
|
|
@ -1446,15 +1446,6 @@ namespace AgroBase.Forms
|
|||
|
||||
Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1);
|
||||
AtualizarDadosTela();
|
||||
|
||||
for (int idxCorredor = 0; idxCorredor < Variaveis.OperacaoEmAndamento.Trajetoria.Corredores.Count; idxCorredor++)
|
||||
{
|
||||
Variaveis.OperacaoEmAndamento.Trajetoria.AtualizarDadosAutonomiaCorredor(
|
||||
idxCorredor: idxCorredor,
|
||||
bat_liberada: true,
|
||||
herb_liberado: true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -2599,24 +2599,12 @@ namespace AgroBase.Models.Modules
|
|||
if (op?.DispAtu?.Dados == null)
|
||||
return tasks;
|
||||
|
||||
// Mantido por compatibilidade com os chamadores antigos.
|
||||
// Task<bool> deriva de Task, então o contrato List<Task> continua válido.
|
||||
tasks.Add(RealizarCalibragemComResultadoAsync(forcar));
|
||||
tasks.Add(ExecutarCalibragemProtegidaAsync(forcar));
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
public Task<bool> RealizarCalibragemComResultadoAsync(bool forcar)
|
||||
{
|
||||
var op = Variaveis.OperacaoEmAndamento;
|
||||
|
||||
if (op?.DispAtu?.Dados == null)
|
||||
return Task.FromResult(false);
|
||||
|
||||
return ExecutarCalibragemProtegidaAsync(forcar);
|
||||
}
|
||||
|
||||
private async Task<bool> ExecutarCalibragemProtegidaAsync(bool forcar)
|
||||
private async Task ExecutarCalibragemProtegidaAsync(bool forcar)
|
||||
{
|
||||
var op = Variaveis.OperacaoEmAndamento;
|
||||
|
||||
|
|
@ -2649,15 +2637,11 @@ namespace AgroBase.Models.Modules
|
|||
|
||||
op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v));
|
||||
}
|
||||
|
||||
return sucesso;
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Calibragem cancelada: {ex.Message}");
|
||||
op?.Sensoriamento?.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Calibragem do módulo {Modulo_ID} cancelada: {ex.Message}");
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AgroBase.Comum;
|
||||
using AgroBase.Comum;
|
||||
using AgroBase.Forms;
|
||||
using AgroBase.Forms.Operacoes;
|
||||
using AgroBase.Models.Modules;
|
||||
|
|
@ -349,12 +349,6 @@ namespace AgroBase.Models
|
|||
private static string _discoverySessionId = Guid.NewGuid().ToString("N");
|
||||
private static long _telemetrySequence = 0;
|
||||
private readonly SemaphoreSlim _calibragemGate = new SemaphoreSlim(1, 1);
|
||||
|
||||
// A calibração inicial é uma etapa transacional da partida da operação.
|
||||
// Depois que ela efetivamente entrou em calibração e terminou, não permitimos
|
||||
// que o timer periódico pare o rover novamente para repetir a mesma rotina.
|
||||
private volatile bool _calibragemInicialConcluida = false;
|
||||
|
||||
public bool CalibrandoRuntime
|
||||
{
|
||||
get
|
||||
|
|
@ -367,12 +361,6 @@ namespace AgroBase.Models
|
|||
private readonly TimeSpan _cooldownCalibragem = TimeSpan.FromSeconds(30);
|
||||
private bool PodeTentarCalibragemAutomatica()
|
||||
{
|
||||
// Depois da calibração inicial efetivamente executada, qualquer pendência
|
||||
// deve permanecer como diagnóstico/alerta. Não paramos o rover novamente
|
||||
// no meio da operação para recalibrar automaticamente.
|
||||
if (_calibragemInicialConcluida)
|
||||
return false;
|
||||
|
||||
return DateTime.Now - _ultimaTentativaCalibragem > _cooldownCalibragem;
|
||||
}
|
||||
private void RegistrarTentativaCalibragem()
|
||||
|
|
@ -1407,11 +1395,6 @@ namespace AgroBase.Models
|
|||
|
||||
op.Sensoriamento.UltimoRegistroLog = agora;
|
||||
op.idxLog = 0;
|
||||
|
||||
// Cada nova operação ganha um ciclo de calibração novo.
|
||||
op._calibragemInicialConcluida = false;
|
||||
op._ultimaTentativaCalibragem = DateTime.MinValue;
|
||||
|
||||
op.IniciarTimerLogsOperacao();
|
||||
|
||||
RedisService.Publish(CmdKey.ManagerWorkerRx, JsonConvert.SerializeObject(new { cmd = ManagerWorkerCommandType.IniciarMPC }));
|
||||
|
|
@ -1682,12 +1665,6 @@ namespace AgroBase.Models
|
|||
if (!await _calibragemGate.WaitAsync(0))
|
||||
return;
|
||||
|
||||
// A calibração forçada do início também conta como tentativa.
|
||||
// Isso evita que o timer periódico considere o cooldown vencido logo
|
||||
// após a partida da operação.
|
||||
if (forcar)
|
||||
op.RegistrarTentativaCalibragem();
|
||||
|
||||
TipoMovimentoDirecional tipoControle = op.Controle.TipoMovimento;
|
||||
_pausaAntesCalibragem = op.Sensoriamento.Operacao.Pausa;
|
||||
bool entrouEmCalibragem = false;
|
||||
|
|
@ -1794,16 +1771,9 @@ namespace AgroBase.Models
|
|||
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem dos módulos iniciada...");
|
||||
|
||||
var tasks = new List<Task>();
|
||||
Task<bool> taskCalibragemAtu = null;
|
||||
|
||||
if (calibragemAtuNecessaria)
|
||||
{
|
||||
// Para o ATU consumimos o resultado real da calibração.
|
||||
// Uma Task concluída não significa necessariamente que bomba/bicos
|
||||
// passaram nos testes.
|
||||
taskCalibragemAtu = dispAtu.Dados.RealizarCalibragemComResultadoAsync(forcar);
|
||||
tasks.Add(taskCalibragemAtu);
|
||||
}
|
||||
tasks.AddRange(dispAtu.Dados.RealizarCalibragemAsync(forcar));
|
||||
|
||||
if (calibragemDirNecessaria)
|
||||
tasks.AddRange(dispMvd.Dados.RealizarCalibragemDirAsync(forcar));
|
||||
|
|
@ -1829,56 +1799,12 @@ namespace AgroBase.Models
|
|||
{
|
||||
await allTasks;
|
||||
|
||||
bool atuSucesso =
|
||||
taskCalibragemAtu == null ||
|
||||
(taskCalibragemAtu.Status == TaskStatus.RanToCompletion && taskCalibragemAtu.Result);
|
||||
Variaveis.MostrarLog(
|
||||
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||
"Tasks de calibragem dos módulos concluídas."
|
||||
);
|
||||
|
||||
var pendencias = op.Sensoriamento?.Operacao?.ModsCalibragem?
|
||||
.SelectMany(modulo =>
|
||||
(modulo.Value ?? new Dictionary<string, bool>())
|
||||
.Where(item => !item.Value)
|
||||
.Select(item => $"{modulo.Key}:{item.Key}")
|
||||
)
|
||||
.ToList()
|
||||
?? new List<string>();
|
||||
|
||||
bool calibragemComPendencias = !atuSucesso || pendencias.Any();
|
||||
|
||||
if (calibragemComPendencias)
|
||||
{
|
||||
string detalhe = pendencias.Any()
|
||||
? string.Join(", ", pendencias)
|
||||
: "ATU reportou pendência sem item individual identificado";
|
||||
|
||||
string mensagem =
|
||||
$"Calibragem inicial concluída com pendências: {detalhe}. " +
|
||||
"A operação será liberada sem nova recalibração automática nesta execução.";
|
||||
|
||||
Variaveis.MostrarLog(
|
||||
"[OperacaoModel.RealizarCalibragemInicialAsync] " + mensagem
|
||||
);
|
||||
|
||||
op.Sensoriamento.InserirLog(
|
||||
T_Code.Mod,
|
||||
StatusModulo.Alerta,
|
||||
50,
|
||||
mensagem
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Variaveis.MostrarLog(
|
||||
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||
"Calibragem dos módulos concluída com sucesso."
|
||||
);
|
||||
|
||||
op.Sensoriamento.InserirLog(
|
||||
T_Code.Mod,
|
||||
StatusModulo.Operante,
|
||||
100,
|
||||
"Calibragem finalizada com sucesso."
|
||||
);
|
||||
}
|
||||
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem finalizada.");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1951,17 +1877,6 @@ namespace AgroBase.Models
|
|||
);
|
||||
}
|
||||
|
||||
if (forcar && entrouEmCalibragem)
|
||||
{
|
||||
op._calibragemInicialConcluida = true;
|
||||
|
||||
Variaveis.MostrarLog(
|
||||
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||
"Ciclo de calibração inicial encerrado. Recalibração automática " +
|
||||
"durante esta operação ficará desabilitada."
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_calibragemGate.Release();
|
||||
|
|
@ -3376,9 +3291,10 @@ namespace AgroBase.Models
|
|||
op.RegistrarTentativaCalibragem();
|
||||
_ = Task.Run(async () => await op.RealizarCalibragemInicialAsync(false));
|
||||
}
|
||||
|
||||
// O estado Calibrando pertence exclusivamente à rotina de calibração.
|
||||
// O timer não deve liberar a operação antes das Tasks terminarem.
|
||||
if (op.CalibrandoRuntime && op.TodosModulosMandatoriosCalibrados())
|
||||
{
|
||||
op.DefinirEstadoCalibragemRuntime(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4224,9 +4140,6 @@ namespace AgroBase.Models
|
|||
OperacaoLiberacaoDebug = RedisService.GetField<JObject>(CtxKey.DadosOperacao, "operacao_liberacao_debug"),
|
||||
StatusOperacaoAtual = (StatusOperacao)RedisService.GetField<int>(CtxKey.DadosOperacao, "status", 0),
|
||||
|
||||
MotivoStatusOperacao = RedisService.GetField<string>(CtxKey.DadosOperacao, "motivo_status_operacao"),
|
||||
UltimoMotivoParada = RedisService.GetField<string>(CtxKey.DadosOperacao, "ultimo_motivo_parada"),
|
||||
|
||||
ModulosMandatorios = new List<OperacaoModulosMandatoriosModel>(op.Parametros.ModulosMandatorios ?? new List<OperacaoModulosMandatoriosModel>()),
|
||||
ParametrosMandatorios = new List<OperacaoParametrosMandatoriosModel>(op.Parametros.ParametrosMandatorios ?? new List<OperacaoParametrosMandatoriosModel>()),
|
||||
DispositivosMapeados = new List<DispositivoDetalhesModel>(SerialService.DispositivosMapeados),
|
||||
|
|
@ -5462,8 +5375,6 @@ namespace AgroBase.Models
|
|||
public StatusOperacao StatusOperacaoAnterior { get; set; }
|
||||
public string ErroOperacaoLiberada { get; set; }
|
||||
public JObject OperacaoLiberacaoDebug { get; set; }
|
||||
public string MotivoStatusOperacao { get; set; }
|
||||
public string UltimoMotivoParada { get; set; }
|
||||
public bool OperacaoConfigurada { get; set; }
|
||||
public bool OperacaoLiberada { get; set; }
|
||||
public bool OperacaoIniciada { get; set; }
|
||||
|
|
@ -5528,8 +5439,6 @@ namespace AgroBase.Models
|
|||
Status = Status,
|
||||
ErroOperacaoLiberada = ErroOperacaoLiberada,
|
||||
OperacaoLiberacaoDebug = OperacaoLiberacaoDebug,
|
||||
MotivoStatusOperacao = MotivoStatusOperacao,
|
||||
UltimoMotivoParada = UltimoMotivoParada,
|
||||
OperacaoConfigurada = OperacaoConfigurada,
|
||||
OperacaoLiberada = OperacaoLiberada,
|
||||
OperacaoIniciada = OperacaoIniciada,
|
||||
|
|
|
|||
|
|
@ -712,8 +712,6 @@ namespace AgroBase.Models.Operacoes
|
|||
Iniciada = op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false,
|
||||
Liberada = op?.Sensoriamento?.Operacao?.OperacaoLiberada ?? false,
|
||||
Status = op?.Sensoriamento?.Operacao?.StatusOperacaoAtual ?? StatusOperacao.Erro,
|
||||
UltimoMotivoParada = op?.Sensoriamento?.Operacao?.UltimoMotivoParada,
|
||||
MotivoStatusOperacao = op?.Sensoriamento?.Operacao?.MotivoStatusOperacao,
|
||||
Modo = parametros?.Modo ?? ModoOperacao.NaoDefinido,
|
||||
ImpedimentoErro = op?.Sensoriamento?.Operacao?.ErroOperacaoLiberada,
|
||||
TempoAguardandoSegs = op?.Sensoriamento?.Operacao?.TempoAguardandoSeg ?? 0,
|
||||
|
|
@ -1499,8 +1497,6 @@ namespace AgroBase.Models.Operacoes
|
|||
public ModoOperacao Modo { get; set; }
|
||||
public StatusOperacao Status { get; set; }
|
||||
public string ImpedimentoErro { get; set; }
|
||||
public string MotivoStatusOperacao { get; set; }
|
||||
public string UltimoMotivoParada { get; set; }
|
||||
public bool Liberada { get; set; }
|
||||
public bool Iniciada { get; set; }
|
||||
public bool Emergencia { get; set; }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1084,215 +1084,87 @@ namespace AgroBase.Models
|
|||
return novoPonto;
|
||||
}
|
||||
|
||||
// V16 - Simulação 4WS unificada com o MPC Python.
|
||||
//
|
||||
// Convenção de alto nível:
|
||||
// anguloControleDeg > 0 => yaw positivo do corpo em qualquer modo.
|
||||
// Em RodasTraseiras isso implica esterçamento físico traseiro invertido.
|
||||
//
|
||||
// Referência cinemática: ponto médio entre os eixos.
|
||||
// velocidadeMs: módulo da velocidade do centro do rover.
|
||||
//
|
||||
// Modelo:
|
||||
// beta = atan((tan(df) + tan(dr)) / 2)
|
||||
// kappaGeom = cos(beta) * (tan(df) - tan(dr)) / L
|
||||
// kappaEf = kappaGeom / (1 + Ku*v²)
|
||||
// omega = v*kappaEf
|
||||
//
|
||||
// Integração analítica com beta e omega constantes no passo.
|
||||
|
||||
public static GPSModel SimularNovaPosicao(
|
||||
TipoMovimentoDirecional tipoMovimento,
|
||||
double anguloControleDeg,
|
||||
double velocidadeMs,
|
||||
double tempoDelta,
|
||||
double anguloAtualDeg,
|
||||
GPSModel pos)
|
||||
public static GPSModel SimularNovaPosicao(TipoMovimentoDirecional tipoMovimento, double anguloControleDeg, double velocidadeMs, double tempoDelta, double anguloAtualDeg, GPSModel pos)
|
||||
{
|
||||
double L = Math.Max(0.05, DistanciaEntreEixosCm / 100.0);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 1) Comando de alto nível -> ângulos FÍSICOS dos dois eixos
|
||||
// ------------------------------------------------------------
|
||||
double dfDeg = 0.0;
|
||||
double drDeg = 0.0;
|
||||
double L = DistanciaEntreEixosCm / 100.0;
|
||||
|
||||
// Defina deltas por modo
|
||||
double dfDeg = 0, drDeg = 0;
|
||||
switch (tipoMovimento)
|
||||
{
|
||||
case TipoMovimentoDirecional.RodasDianteiras:
|
||||
dfDeg = anguloControleDeg;
|
||||
drDeg = 0.0;
|
||||
break;
|
||||
|
||||
case TipoMovimentoDirecional.RodasTraseiras:
|
||||
// Importante:
|
||||
// +angulo de comando significa +yaw do corpo.
|
||||
// Para obter o mesmo yaw usando apenas o eixo traseiro,
|
||||
// a roda traseira precisa esterçar fisicamente no sentido oposto.
|
||||
dfDeg = 0.0;
|
||||
drDeg = -anguloControleDeg;
|
||||
break;
|
||||
|
||||
case TipoMovimentoDirecional.MovimentoArco:
|
||||
dfDeg = anguloControleDeg;
|
||||
drDeg = -anguloControleDeg;
|
||||
break;
|
||||
|
||||
case TipoMovimentoDirecional.MovimentoDiagonal:
|
||||
case TipoMovimentoDirecional.MovimentoLateral:
|
||||
// Mesma família cinemática: eixos em fase, sem yaw.
|
||||
dfDeg = anguloControleDeg;
|
||||
drDeg = anguloControleDeg;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Fallback conservador: dianteiras.
|
||||
dfDeg = anguloControleDeg;
|
||||
drDeg = 0.0;
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
double df = dfDeg * Math.PI / 180.0;
|
||||
double dr = drDeg * Math.PI / 180.0;
|
||||
// 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 tf = Math.Tan(df);
|
||||
double tr = Math.Tan(dr);
|
||||
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;
|
||||
|
||||
if (Math.Abs(tf) < 1e-12) tf = 0.0;
|
||||
if (Math.Abs(tr) < 1e-12) tr = 0.0;
|
||||
// Correção dinâmica (opcional)
|
||||
double Ku = 2.5; // ajuste fino em campo
|
||||
double kappaEf = kappa / (1 + Ku * velocidadeMs * velocidadeMs);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 2) Cinemática 4WS no ponto médio entre eixos
|
||||
// ------------------------------------------------------------
|
||||
double beta = Math.Atan(0.5 * (tf + tr));
|
||||
|
||||
// velocidadeMs é o módulo da velocidade do centro.
|
||||
// Por isso entra cos(beta) na curvatura de yaw por metro percorrido.
|
||||
double kappaGeom =
|
||||
Math.Cos(beta) * (tf - tr) / L;
|
||||
|
||||
if (Math.Abs(kappaGeom) < 1e-12)
|
||||
kappaGeom = 0.0;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3) Correção dinâmica
|
||||
// ------------------------------------------------------------
|
||||
// Deve ser o MESMO valor usado em:
|
||||
// mpc_v16 -> cinematica_4ws_ku
|
||||
double Ku = 2.5;
|
||||
|
||||
double kappaEf =
|
||||
kappaGeom /
|
||||
(1.0 + Ku * velocidadeMs * velocidadeMs);
|
||||
|
||||
double omega = velocidadeMs * kappaEf; // rad/s
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 4) Integração analítica
|
||||
// ------------------------------------------------------------
|
||||
// Convenção:
|
||||
// theta = heading do corpo, 0=Norte, +90°=Leste.
|
||||
// A velocidade do centro aponta para theta + beta.
|
||||
double theta0 = anguloAtualDeg * Math.PI / 180.0;
|
||||
// Integração
|
||||
double theta = anguloAtualDeg * Math.PI / 180.0;
|
||||
double omega = velocidadeMs * kappaEf; // rad/s
|
||||
double dtheta = omega * tempoDelta;
|
||||
double theta1 = theta0 + dtheta;
|
||||
theta += dtheta;
|
||||
|
||||
double phi0 = theta0 + beta;
|
||||
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
|
||||
|
||||
double dx;
|
||||
double dy;
|
||||
|
||||
if (Math.Abs(omega) <= 1e-9)
|
||||
{
|
||||
double distancia = velocidadeMs * tempoDelta;
|
||||
|
||||
dx = Math.Sin(phi0) * distancia; // Leste(+)
|
||||
dy = Math.Cos(phi0) * distancia; // Norte(+)
|
||||
}
|
||||
else
|
||||
{
|
||||
double phi1 = theta1 + beta;
|
||||
double raioVel = velocidadeMs / omega;
|
||||
|
||||
dx =
|
||||
raioVel *
|
||||
(Math.Cos(phi0) - Math.Cos(phi1));
|
||||
|
||||
dy =
|
||||
raioVel *
|
||||
(Math.Sin(phi1) - Math.Sin(phi0));
|
||||
}
|
||||
// >>> 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;
|
||||
if (pos == null) return ultimaPosicao;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 5) Plano local -> geográfico
|
||||
// ------------------------------------------------------------
|
||||
// geo
|
||||
double R_earth = GPSUtils.RaioDaTerra;
|
||||
|
||||
double dLat =
|
||||
(dy / R_earth) *
|
||||
180.0 / Math.PI;
|
||||
|
||||
double cosLat =
|
||||
Math.Cos(pos.LatitudeAnt * Math.PI / 180.0);
|
||||
|
||||
// Proteção só para evitar divisão numérica absurda.
|
||||
if (Math.Abs(cosLat) < 1e-9)
|
||||
cosLat = cosLat >= 0 ? 1e-9 : -1e-9;
|
||||
|
||||
double dLon =
|
||||
(dx / (R_earth * cosLat)) *
|
||||
180.0 / Math.PI;
|
||||
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 agoraMono = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
|
||||
|
||||
double orientacaoRealDeg =
|
||||
GPSUtils.NormalizarAngulo(
|
||||
theta1 * 180.0 / Math.PI
|
||||
);
|
||||
|
||||
// Direção instantânea de deslocamento do centro no final do passo.
|
||||
double orientacaoMovimentoDeg =
|
||||
GPSUtils.NormalizarAngulo(
|
||||
(theta1 + beta) * 180.0 / Math.PI
|
||||
);
|
||||
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,
|
||||
|
||||
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(),
|
||||
};
|
||||
|
||||
// Mantém exatamente o lever-arm já usado no simulador real.
|
||||
var corrigida =
|
||||
GPSService.LeverArm.FixLeverArmLatLon_Fast(
|
||||
novaPosicao.LatitudeAnt,
|
||||
|
|
@ -1300,13 +1172,16 @@ namespace AgroBase.Models
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -576,16 +576,10 @@ class CameraMultispectral:
|
|||
)
|
||||
|
||||
try:
|
||||
if stream is not None:
|
||||
fechar_stream = getattr(stream, "fechar", None)
|
||||
if not callable(fechar_stream):
|
||||
fechar_stream = getattr(stream, "parar", None)
|
||||
if callable(fechar_stream):
|
||||
fechar_stream()
|
||||
except Exception as e:
|
||||
self.mostrar_log(
|
||||
f"[CameraMultispectral] Erro ao fechar stream TCP: {e}"
|
||||
)
|
||||
if stream is not None and hasattr(stream, "parar"):
|
||||
stream.parar()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with self._lock:
|
||||
self.ultimo_tensor_multispec = None
|
||||
|
|
|
|||
|
|
@ -223,19 +223,6 @@ class CameraOak:
|
|||
self.rodando = False
|
||||
self.iniciado = False
|
||||
|
||||
stream = self.stream
|
||||
self.stream = None
|
||||
|
||||
try:
|
||||
if stream is not None:
|
||||
fechar_stream = getattr(stream, "fechar", None)
|
||||
if not callable(fechar_stream):
|
||||
fechar_stream = getattr(stream, "parar", None)
|
||||
if callable(fechar_stream):
|
||||
fechar_stream()
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"Erro ao fechar stream TCP da câmera: {e}")
|
||||
|
||||
try:
|
||||
if self._cache_thread is not None and self._cache_thread.is_alive():
|
||||
self._cache_thread.join(timeout=1.0)
|
||||
|
|
@ -295,6 +282,10 @@ class CameraOak:
|
|||
"frame_valido": False,
|
||||
}
|
||||
|
||||
try:
|
||||
self.stream = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _is_erro_fatal_depthai(self, erro):
|
||||
if erro is None:
|
||||
|
|
|
|||
|
|
@ -208,8 +208,6 @@ class OakFcc3Manager:
|
|||
"dropped_queue": 0,
|
||||
"last_error": None,
|
||||
"last_loop_ms": 0.0,
|
||||
"last_packet_age_ms": None,
|
||||
"_last_frame_seen_perf_by_cam": {},
|
||||
}
|
||||
|
||||
def __enter__(self):
|
||||
|
|
@ -1635,15 +1633,6 @@ class OakFcc3Manager:
|
|||
|
||||
t0_get = self._cap_now_ms()
|
||||
msg = q.get()
|
||||
|
||||
try:
|
||||
seen = self._capture_thread_stats.setdefault(
|
||||
"_last_frame_seen_perf_by_cam", {}
|
||||
)
|
||||
seen[cam_id] = time.perf_counter()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if perf is not None:
|
||||
perf["drain_get_msg_ms"] += self._cap_now_ms() - t0_get
|
||||
|
||||
|
|
@ -2354,7 +2343,6 @@ class OakFcc3Manager:
|
|||
"last_error": None,
|
||||
"last_loop_ms": 0.0,
|
||||
"last_packet_age_ms": None,
|
||||
"_last_frame_seen_perf_by_cam": {},
|
||||
}
|
||||
|
||||
def _start_async_capture_thread(self):
|
||||
|
|
@ -2429,11 +2417,6 @@ class OakFcc3Manager:
|
|||
# Importante: esta thread é a única que mexe nas queues/buffers.
|
||||
self._drain_queues_to_buffers(perf=perf)
|
||||
|
||||
if perf is not None:
|
||||
perf["buffer_lengths_after_drain"] = (
|
||||
self._buffer_lengths_snapshot()
|
||||
)
|
||||
|
||||
# Em RAW_BRUTO + async, o produtor publica apenas descritores
|
||||
# ImgFrame da tripleta sincronizada. O consumidor materializa
|
||||
# somente o pacote latest que realmente pegar. Isso elimina
|
||||
|
|
@ -2449,25 +2432,6 @@ class OakFcc3Manager:
|
|||
)
|
||||
|
||||
if synced is None:
|
||||
# Guarda diagnóstico do motivo pelo qual o sincronizador
|
||||
# ainda não conseguiu formar um pacote. Isso aparece no
|
||||
# status usado pelo watchdog do WeedWorker e permite saber
|
||||
# se uma cabeça específica ficou sem frames ou se houve
|
||||
# descarte por sincronismo estrito.
|
||||
if perf is not None:
|
||||
self._capture_thread_stats["last_wait_reason"] = (
|
||||
perf.get("wait_reason")
|
||||
)
|
||||
self._capture_thread_stats["last_buffer_lengths"] = dict(
|
||||
perf.get("buffer_lengths_after_drain") or {}
|
||||
)
|
||||
self._capture_thread_stats["last_drained_by_cam"] = dict(
|
||||
perf.get("drained_by_cam") or {}
|
||||
)
|
||||
self._capture_thread_stats["last_queue_has_true_by_cam"] = dict(
|
||||
perf.get("queue_has_true_by_cam") or {}
|
||||
)
|
||||
|
||||
# Dorme curto. Pode testar 0.0005 se quiser reduzir latência.
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
|
|
@ -2539,10 +2503,6 @@ class OakFcc3Manager:
|
|||
|
||||
self._capture_thread_stats["packets"] += 1
|
||||
self._capture_thread_stats["last_error"] = None
|
||||
self._capture_thread_stats["last_wait_reason"] = "packet_ready"
|
||||
self._capture_thread_stats["last_buffer_lengths"] = (
|
||||
self._buffer_lengths_snapshot()
|
||||
)
|
||||
self._capture_thread_stats["last_loop_ms"] = (time.perf_counter() - t_loop0) * 1000.0
|
||||
|
||||
self._capture_cond.notify_all()
|
||||
|
|
@ -2590,20 +2550,6 @@ class OakFcc3Manager:
|
|||
latest_age_ms = (time.perf_counter() - float(self._latest_packet.get("created_perf_counter", 0.0))) * 1000.0
|
||||
|
||||
st = dict(getattr(self, "_capture_thread_stats", {}) or {})
|
||||
|
||||
last_seen_perf = st.pop(
|
||||
"_last_frame_seen_perf_by_cam", {}
|
||||
) or {}
|
||||
now_perf = time.perf_counter()
|
||||
camera_frame_age_ms = {
|
||||
str(cam_id): round(
|
||||
max(0.0, now_perf - float(ts)) * 1000.0,
|
||||
1,
|
||||
)
|
||||
for cam_id, ts in last_seen_perf.items()
|
||||
if ts is not None
|
||||
}
|
||||
|
||||
st.update({
|
||||
"enabled": bool(getattr(self, "async_capture_enabled", True)),
|
||||
"mode": str(getattr(self, "async_capture_mode", "latest")),
|
||||
|
|
@ -2613,8 +2559,5 @@ class OakFcc3Manager:
|
|||
"last_consumed_seq": int(getattr(self, "_last_consumed_packet_seq", 0)),
|
||||
"queue_len": int(len(getattr(self, "_packet_queue", []))),
|
||||
"latest_age_ms": latest_age_ms,
|
||||
"last_packet_age_ms": latest_age_ms,
|
||||
"buffer_lengths": self._buffer_lengths_snapshot(),
|
||||
"camera_frame_age_ms": camera_frame_age_ms,
|
||||
})
|
||||
return st
|
||||
|
|
|
|||
|
|
@ -1572,10 +1572,6 @@ class CameraTcpStreamer:
|
|||
stream_fps=0.0,
|
||||
)
|
||||
|
||||
def parar(self):
|
||||
"""Alias de compatibilidade para encerramento explícito."""
|
||||
self.fechar()
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.fechar()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1651,25 +1651,14 @@ class CameraManager:
|
|||
threading.Thread(target=loop, daemon=True).start()
|
||||
|
||||
def _iniciar_loop_frame_stream(self, freq=2.0):
|
||||
"""
|
||||
Alimenta o CameraTcpStreamer sem duplicar a política de FPS.
|
||||
|
||||
O streamer é a única camada responsável por throttle, banda,
|
||||
backpressure e reconexão. Este produtor apenas oferece o último frame
|
||||
em frequência superior ao maior FPS operacional.
|
||||
"""
|
||||
producer_hz = max(10.0, float(freq or 2.0) * 2.0)
|
||||
|
||||
def loop():
|
||||
while True:
|
||||
t_wall0 = time.time()
|
||||
t_loop0 = time.perf_counter()
|
||||
stream_on = False
|
||||
|
||||
try:
|
||||
camera_atual = self.camera
|
||||
if camera_atual is None:
|
||||
time.sleep(0.25)
|
||||
if self.camera is None:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
|
||||
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
|
||||
|
|
@ -1684,7 +1673,7 @@ class CameraManager:
|
|||
frame = self._get_cached_preview_frame(frame_type, copiar=False)
|
||||
|
||||
if frame is not None:
|
||||
camera_atual.enviar_frame_tcp(frame)
|
||||
self.camera.enviar_frame_tcp(frame)
|
||||
|
||||
if self.debug_visual and self._visual_disponivel_para_frame():
|
||||
self._solicitar_preview(TipoFrameCamera.Debug)
|
||||
|
|
@ -1703,14 +1692,15 @@ class CameraManager:
|
|||
|
||||
finally:
|
||||
lat = time.time() - t_wall0
|
||||
hz = producer_hz if stream_on else 2.0
|
||||
time.sleep(max(0.0, (1.0 / hz) - lat))
|
||||
|
||||
threading.Thread(
|
||||
target=loop,
|
||||
daemon=True,
|
||||
name="visual-stream-feed",
|
||||
).start()
|
||||
try:
|
||||
freq_atual = float(getattr(self.camera.stream, "_op_fps", freq) or freq)
|
||||
except Exception:
|
||||
freq_atual = freq
|
||||
|
||||
time.sleep(max(0.0, (1.0 / max(freq_atual, 0.1)) - lat))
|
||||
|
||||
threading.Thread(target=loop, daemon=True).start()
|
||||
|
||||
def _iniciar_loop_analise_continua(self, freq=8.0):
|
||||
def loop():
|
||||
|
|
|
|||
|
|
@ -3231,26 +3231,14 @@ class CameraManager:
|
|||
threading.Thread(target=loop, daemon=True).start()
|
||||
|
||||
def _iniciar_loop_frame_stream(self, freq=2.0):
|
||||
"""
|
||||
Alimenta o CameraTcpStreamer com o último preview disponível.
|
||||
|
||||
Importante: o controle de FPS/banda existe SOMENTE dentro do
|
||||
CameraTcpStreamer. Este loop roda um pouco mais rápido que o maior
|
||||
FPS operacional para não criar um segundo gate sincronizado que possa
|
||||
pular janelas inteiras por jitter de agendamento.
|
||||
"""
|
||||
producer_hz = max(10.0, float(freq or 2.0) * 2.0)
|
||||
|
||||
def loop():
|
||||
while True:
|
||||
t0 = time.time()
|
||||
t_loop0 = time.perf_counter()
|
||||
stream_on = False
|
||||
|
||||
try:
|
||||
camera_atual = self.camera
|
||||
if camera_atual is None:
|
||||
time.sleep(0.25)
|
||||
if self.camera is None:
|
||||
time.sleep(5.0)
|
||||
continue
|
||||
|
||||
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
|
||||
|
|
@ -3260,11 +3248,9 @@ class CameraManager:
|
|||
frame_type = TipoFrameCamera(
|
||||
camera_ctx.get("frame_type", TipoFrameCamera.Rgb.value)
|
||||
)
|
||||
|
||||
# Stream é apenas consumidor do cache pronto.
|
||||
# Stream também é consumidor de cache. Não monta preview.
|
||||
frame = self._get_cached_preview_frame_ref(frame_type)
|
||||
if frame is not None:
|
||||
camera_atual.enviar_frame_tcp(frame)
|
||||
self.camera.enviar_frame_tcp(frame)
|
||||
|
||||
if self.debug_visual:
|
||||
dbg = self._get_cached_preview_frame_ref(TipoFrameCamera.Debug)
|
||||
|
|
@ -3284,14 +3270,15 @@ class CameraManager:
|
|||
)
|
||||
|
||||
lat = time.time() - t0
|
||||
hz = producer_hz if stream_on else 2.0
|
||||
time.sleep(max(0.0, (1.0 / hz) - lat))
|
||||
|
||||
threading.Thread(
|
||||
target=loop,
|
||||
daemon=True,
|
||||
name="weed-stream-feed",
|
||||
).start()
|
||||
try:
|
||||
freq_atual = float(getattr(self.camera.stream, "_op_fps", freq) or freq)
|
||||
except Exception:
|
||||
freq_atual = freq
|
||||
|
||||
time.sleep(max(0.0, (1.0 / freq_atual) - lat))
|
||||
|
||||
threading.Thread(target=loop, daemon=True).start()
|
||||
|
||||
def _iniciar_loop_analise_continua(self, freq=15.0):
|
||||
def loop():
|
||||
|
|
|
|||
|
|
@ -537,7 +537,6 @@ namespace OperationControl.ViewModels
|
|||
public void AtualizarBarraSuperior(OperacaoParametrosModel rover)
|
||||
{
|
||||
var obj = rover?.DadosLeitura;
|
||||
|
||||
if (rover == null || obj == null)
|
||||
return;
|
||||
|
||||
|
|
@ -546,76 +545,12 @@ namespace OperationControl.ViewModels
|
|||
|
||||
var eventos = new List<MotivoTopBarModel>();
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
|
||||
string NormalizarTexto(string texto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(texto))
|
||||
return null;
|
||||
|
||||
var partes = texto
|
||||
.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x?.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return partes.Count > 0
|
||||
? string.Join(" • ", partes)
|
||||
: null;
|
||||
}
|
||||
|
||||
bool EhModuloMandatorio(T_Code modulo)
|
||||
{
|
||||
// A telemetria recebida do rover é a fonte principal.
|
||||
// Só cai para a configuração local caso o snapshot não exista.
|
||||
var modulosMandatorios =
|
||||
obj.Operacao?.ModulosMandatorios
|
||||
?? rover.ModulosMandatorios;
|
||||
|
||||
return modulosMandatorios?.Any(x =>
|
||||
(x?.Dispositivo ?? T_Code.Vzo) == modulo &&
|
||||
(x?.Utilizar ?? false) &&
|
||||
(x?.Mandatorio ?? false)
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
string MotivoOperacaoAtual()
|
||||
{
|
||||
// 1) Motivo autoritativo de NÃO liberação.
|
||||
var motivo = NormalizarTexto(obj.Operacao?.ImpedimentoErro);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(motivo))
|
||||
return motivo;
|
||||
|
||||
// 2) Motivo do estado/status atual.
|
||||
motivo = NormalizarTexto(obj.Operacao?.MotivoStatusOperacao);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(motivo))
|
||||
return motivo;
|
||||
|
||||
// 3) Último motivo de parada apenas como fallback.
|
||||
motivo = NormalizarTexto(obj.Operacao?.UltimoMotivoParada);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(motivo))
|
||||
return motivo;
|
||||
|
||||
return "A operação não foi liberada pelo rover.";
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 0) Comunicação
|
||||
// ============================================================
|
||||
|
||||
double tempoSemResposta =
|
||||
(DateTime.Now - rover.UltimoContato).TotalSeconds;
|
||||
|
||||
bool semComunicacao =
|
||||
!AppShell.Mock &&
|
||||
tempoSemResposta >= VariaveisControleOperacao.TempoRoverVivo;
|
||||
double tempoSemResposta = (DateTime.Now - rover.UltimoContato).TotalSeconds;
|
||||
bool semComunicacao = !AppShell.Mock && tempoSemResposta >= VariaveisControleOperacao.TempoRoverVivo;
|
||||
|
||||
// ============================
|
||||
// 0) Perda de comunicação
|
||||
// ============================
|
||||
if (semComunicacao)
|
||||
{
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
|
|
@ -623,116 +558,63 @@ namespace OperationControl.ViewModels
|
|||
Prioridade = 2000,
|
||||
Fonte = "Comunicacao",
|
||||
Titulo = "PERDA DE COMUNICAÇÃO COM O ROVER",
|
||||
Descricao =
|
||||
$"Sem resposta do rover há {tempoSemResposta:F1} s. " +
|
||||
"Tentando reconectar automaticamente.",
|
||||
Descricao = $"Sem resposta do rover há {tempoSemResposta:F1} s. Tentando reconectar automaticamente.",
|
||||
StatusVisual = StatusModulo.Desconectado
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 1) NPC / núcleo central
|
||||
// ============================================================
|
||||
|
||||
bool todosModulosDesconectados =
|
||||
obj.ModulosSaude?.Any() == true &&
|
||||
obj.ModulosSaude.All(x =>
|
||||
(x?.status ?? StatusModulo.Desconectado) ==
|
||||
StatusModulo.Desconectado);
|
||||
|
||||
if (todosModulosDesconectados)
|
||||
// ============================
|
||||
// 0) NCP desconectado
|
||||
// ============================
|
||||
if (obj.ModulosSaude?.All(x => (x?.status ?? StatusModulo.Desconectado) == StatusModulo.Desconectado) ?? false)
|
||||
{
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
{
|
||||
Prioridade = 1100,
|
||||
Fonte = "NCP",
|
||||
Titulo = "NÚCLEO CENTRAL DE PROCESSAMENTO DESCONECTADO",
|
||||
Descricao =
|
||||
"O núcleo central de processamento foi desconectado. " +
|
||||
"Tentando reconectar...",
|
||||
Descricao = "O núcleo central de processamento foi desconectado, tentando reconectar...",
|
||||
StatusVisual = StatusModulo.Falha
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2) Emergência
|
||||
// ============================================================
|
||||
|
||||
// ============================
|
||||
// 1) Emergência
|
||||
// ============================
|
||||
if (obj.Operacao?.Emergencia == true)
|
||||
{
|
||||
var motivo =
|
||||
NormalizarTexto(obj.Operacao.MotivoStatusOperacao)
|
||||
?? "Parada de emergência solicitada.";
|
||||
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
{
|
||||
Prioridade = 1000,
|
||||
Fonte = "Emergencia",
|
||||
Titulo = "OPERAÇÃO BLOQUEADA POR EMERGÊNCIA",
|
||||
Descricao = motivo,
|
||||
Descricao = "Parada de emergência solicitada.",
|
||||
StatusVisual = StatusModulo.Falha
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3) Pausa
|
||||
// ============================================================
|
||||
|
||||
// ============================
|
||||
// 2) Pausa
|
||||
// ============================
|
||||
if (obj.Operacao?.Pausa == true)
|
||||
{
|
||||
var motivo =
|
||||
NormalizarTexto(obj.Operacao.MotivoStatusOperacao)
|
||||
?? "Pausa operacional solicitada.";
|
||||
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
{
|
||||
Prioridade = 900,
|
||||
Fonte = "Pausa",
|
||||
Titulo = "OPERAÇÃO PAUSADA",
|
||||
Descricao = motivo,
|
||||
Descricao = "Pausa operacional solicitada.",
|
||||
StatusVisual = StatusModulo.Alerta
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 4) Operação não liberada
|
||||
//
|
||||
// Essa é a informação autoritativa vinda do rover.
|
||||
// Exemplo:
|
||||
// Sen: Desconectado
|
||||
// Gps: Falha
|
||||
// Parâmetro obrigatório ausente
|
||||
// ============================================================
|
||||
|
||||
if (
|
||||
obj.Operacao != null &&
|
||||
!obj.Operacao.Liberada &&
|
||||
obj.Operacao.Pausa != true &&
|
||||
obj.Operacao.Emergencia != true
|
||||
)
|
||||
{
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
{
|
||||
Prioridade = 850,
|
||||
Fonte = "Operacao",
|
||||
Titulo = "OPERAÇÃO BLOQUEADA",
|
||||
Descricao = MotivoOperacaoAtual(),
|
||||
StatusVisual = StatusModulo.Falha
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 5) Controle
|
||||
// ============================================================
|
||||
|
||||
var motivosControle =
|
||||
obj.Controle?.Motivos?
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(NormalizarTexto)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.ToList()
|
||||
?? new List<string>();
|
||||
// ============================
|
||||
// 3) Controle
|
||||
// ============================
|
||||
var motivosControle = obj.Controle?.Motivos?
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.ToList() ?? new List<string>();
|
||||
|
||||
foreach (var motivo in motivosControle)
|
||||
{
|
||||
|
|
@ -746,18 +628,13 @@ namespace OperationControl.ViewModels
|
|||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 6) Trajetória / autonomia
|
||||
// ============================================================
|
||||
|
||||
var motivosTrajetoria =
|
||||
obj.Trajetoria?.AutonomiaCorredor?.Motivos?
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(NormalizarTexto)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.ToList()
|
||||
?? new List<string>();
|
||||
// ============================
|
||||
// 4) Trajetória / autonomia
|
||||
// ============================
|
||||
var motivosTrajetoria = obj.Trajetoria?.AutonomiaCorredor?.Motivos?
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.ToList() ?? new List<string>();
|
||||
|
||||
foreach (var motivo in motivosTrajetoria)
|
||||
{
|
||||
|
|
@ -771,215 +648,110 @@ namespace OperationControl.ViewModels
|
|||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 7) Estado operacional
|
||||
//
|
||||
// Aqui pegamos estados legítimos em que a operação está
|
||||
// liberada, porém ainda não está executando.
|
||||
//
|
||||
// Exemplos:
|
||||
// Aguardando retomada segura
|
||||
// Parametrizando
|
||||
// Finalizando
|
||||
// ============================================================
|
||||
|
||||
if (
|
||||
obj.Operacao != null &&
|
||||
obj.Operacao.Liberada &&
|
||||
obj.Operacao.Pausa != true &&
|
||||
obj.Operacao.Emergencia != true &&
|
||||
obj.Operacao.Status != StatusOperacao.EmAndamento &&
|
||||
!string.IsNullOrWhiteSpace(obj.Operacao.MotivoStatusOperacao)
|
||||
)
|
||||
{
|
||||
var motivo =
|
||||
NormalizarTexto(obj.Operacao.MotivoStatusOperacao);
|
||||
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
{
|
||||
Prioridade = 650,
|
||||
Fonte = "StatusOperacao",
|
||||
Titulo =
|
||||
$"OPERAÇÃO {obj.Operacao.Status.ToString().ToUpper()}",
|
||||
Descricao = motivo,
|
||||
StatusVisual =
|
||||
obj.Operacao.Status == StatusOperacao.Erro
|
||||
? StatusModulo.Falha
|
||||
: StatusModulo.Alerta
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 8) Módulos críticos
|
||||
// ============================================================
|
||||
|
||||
var modulosCriticos =
|
||||
obj.ModulosSaude?
|
||||
.Where(mod =>
|
||||
{
|
||||
if (mod == null)
|
||||
return false;
|
||||
|
||||
var status =
|
||||
mod?.status ?? StatusModulo.Desconectado;
|
||||
|
||||
bool temCondicaoSevera =
|
||||
mod?.condicoes_operacionais?.Any(
|
||||
cond => cond?.severidade > 90
|
||||
) ?? false;
|
||||
|
||||
bool mandatorio =
|
||||
EhModuloMandatorio(mod?.modulo ?? T_Code.Vzo);
|
||||
|
||||
bool naoOperante =
|
||||
!new[]
|
||||
{
|
||||
StatusModulo.Operante,
|
||||
StatusModulo.Alerta
|
||||
}.Contains(status);
|
||||
|
||||
bool emAlerta =
|
||||
status == StatusModulo.Alerta;
|
||||
|
||||
return
|
||||
temCondicaoSevera ||
|
||||
(mandatorio && naoOperante) ||
|
||||
(mandatorio && emAlerta);
|
||||
})
|
||||
.ToList()
|
||||
?? new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>();
|
||||
// ============================
|
||||
// 5) Módulos com condições operacionais críticas
|
||||
// ============================
|
||||
var modulosCriticos = obj.ModulosSaude?
|
||||
.Where(mod =>
|
||||
(mod?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false) ||
|
||||
(
|
||||
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(mod?.status ?? StatusModulo.Desconectado) &&
|
||||
(rover?.ModulosMandatorios?.Any(x =>
|
||||
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) &&
|
||||
(x?.Utilizar ?? false) &&
|
||||
(x?.Mandatorio ?? false)
|
||||
) ?? false)
|
||||
) ||
|
||||
(
|
||||
(mod?.status ?? StatusModulo.Desconectado) == StatusModulo.Alerta &&
|
||||
(rover?.ModulosMandatorios?.Any(x =>
|
||||
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) &&
|
||||
(x?.Utilizar ?? false) &&
|
||||
(x?.Mandatorio ?? false)
|
||||
) ?? false)
|
||||
)
|
||||
)
|
||||
.ToList() ?? new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>();
|
||||
|
||||
foreach (var modulo in modulosCriticos)
|
||||
{
|
||||
var codigoModulo =
|
||||
modulo?.modulo ?? T_Code.Vzo;
|
||||
|
||||
var nomeModulo =
|
||||
codigoModulo.ToString();
|
||||
|
||||
var status =
|
||||
modulo?.status ?? StatusModulo.Desconectado;
|
||||
|
||||
bool mandatorio =
|
||||
EhModuloMandatorio(codigoModulo);
|
||||
|
||||
bool temCondicaoSevera =
|
||||
modulo?.condicoes_operacionais?.Any(
|
||||
cond => cond?.severidade > 90
|
||||
) ?? false;
|
||||
var nomeModulo = (modulo?.modulo ?? T_Code.Vzo).ToString();
|
||||
|
||||
bool temCondicaoSevera = modulo?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false;
|
||||
bool moduloMandatorioNaoOperante =
|
||||
mandatorio &&
|
||||
status != StatusModulo.Operante &&
|
||||
status != StatusModulo.Alerta;
|
||||
|
||||
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(modulo?.status ?? StatusModulo.Desconectado) &&
|
||||
(rover?.ModulosMandatorios?.Any(x =>
|
||||
(x?.Dispositivo ?? T_Code.Vzo) == (modulo?.modulo ?? T_Code.Vzo) &&
|
||||
(x?.Utilizar ?? false) &&
|
||||
(x?.Mandatorio ?? false)
|
||||
) ?? false);
|
||||
bool moduloMandatorioAlerta =
|
||||
mandatorio &&
|
||||
status == StatusModulo.Alerta;
|
||||
|
||||
// --------------------------------------------------------
|
||||
// 8.1) Mandatório não operacional
|
||||
// --------------------------------------------------------
|
||||
(modulo?.status ?? StatusModulo.Desconectado) == StatusModulo.Alerta &&
|
||||
(rover?.ModulosMandatorios?.Any(x =>
|
||||
(x?.Dispositivo ?? T_Code.Vzo) == (modulo?.modulo ?? T_Code.Vzo) &&
|
||||
(x?.Utilizar ?? false) &&
|
||||
(x?.Mandatorio ?? false)
|
||||
) ?? false);
|
||||
|
||||
// Caso 1: módulo mandatório não operante
|
||||
if (moduloMandatorioNaoOperante)
|
||||
{
|
||||
List<string> motivos =
|
||||
modulo?.motivos?
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(NormalizarTexto)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.ToList()
|
||||
?? new List<string>();
|
||||
|
||||
string motivoTexto =
|
||||
motivos.Count > 0
|
||||
? string.Join("; ", motivos)
|
||||
: "Sem motivo detalhado informado.";
|
||||
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
|
||||
string motivos = string.Join("; ", modulo?.motivos ?? new List<string>() { "Desconhecido" });
|
||||
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
{
|
||||
Prioridade = 620,
|
||||
Fonte = "ModuloCritico",
|
||||
Titulo = "MÓDULO MANDATÓRIO NÃO OPERACIONAL",
|
||||
Descricao =
|
||||
$"{nomeModulo}: módulo mandatório em estado " +
|
||||
$"'{status}': {motivoTexto}",
|
||||
Descricao = $"{nomeModulo}: módulo mandatório em estado '{statusTexto}': {motivos}",
|
||||
StatusVisual = StatusModulo.Falha,
|
||||
Modulo = codigoModulo
|
||||
Modulo = modulo?.modulo
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// 8.2) Condições operacionais severas
|
||||
// --------------------------------------------------------
|
||||
|
||||
// Caso 2: condições operacionais severas
|
||||
if (temCondicaoSevera)
|
||||
{
|
||||
var descricoesCriticas =
|
||||
modulo?.condicoes_operacionais?
|
||||
.Where(cond => cond?.severidade > 90)
|
||||
.Select(cond =>
|
||||
NormalizarTexto(cond?.descricao))
|
||||
.Where(desc =>
|
||||
!string.IsNullOrWhiteSpace(desc))
|
||||
.Distinct()
|
||||
.ToList()
|
||||
?? new List<string>();
|
||||
var descricoesCriticas = modulo?.condicoes_operacionais?
|
||||
.Where(cond => cond?.severidade > 90)
|
||||
.Select(cond => cond?.descricao ?? "")
|
||||
.Where(desc => !string.IsNullOrWhiteSpace(desc))
|
||||
.Distinct()
|
||||
.ToList() ?? new List<string>();
|
||||
|
||||
foreach (var descricao in descricoesCriticas)
|
||||
foreach (var desc in descricoesCriticas)
|
||||
{
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
{
|
||||
Prioridade = 610,
|
||||
Fonte = "ModuloCritico",
|
||||
Titulo = "CONDIÇÕES OPERACIONAIS CRÍTICAS",
|
||||
Descricao =
|
||||
$"{nomeModulo}: {descricao}",
|
||||
Descricao = $"{nomeModulo}: {desc}",
|
||||
StatusVisual = StatusModulo.Alerta,
|
||||
Modulo = codigoModulo
|
||||
Modulo = modulo?.modulo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// 8.3) Mandatório em alerta
|
||||
// --------------------------------------------------------
|
||||
|
||||
// Caso 3: módulo mandatório em alerta
|
||||
if (moduloMandatorioAlerta)
|
||||
{
|
||||
List<string> motivos =
|
||||
modulo?.motivos?
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(NormalizarTexto)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.ToList()
|
||||
?? new List<string>();
|
||||
|
||||
string motivoTexto =
|
||||
motivos.Count > 0
|
||||
? string.Join("; ", motivos)
|
||||
: "Sem motivo detalhado informado.";
|
||||
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
|
||||
string motivos = string.Join("; ", modulo?.motivos ?? new List<string>() { "Desconhecido" });
|
||||
|
||||
eventos.Add(new MotivoTopBarModel
|
||||
{
|
||||
Prioridade = 600,
|
||||
Fonte = "ModuloCritico",
|
||||
Titulo = "MÓDULO MANDATÓRIO EM ALERTA",
|
||||
Descricao =
|
||||
$"{nomeModulo}: módulo mandatório em estado " +
|
||||
$"'{status}': {motivoTexto}",
|
||||
Descricao = $"{nomeModulo}: módulo mandatório em estado '{statusTexto}': {motivos}",
|
||||
StatusVisual = StatusModulo.Alerta,
|
||||
Modulo = codigoModulo
|
||||
Modulo = modulo?.modulo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 9) Ordenação dos módulos
|
||||
// ============================================================
|
||||
|
||||
var ordemModulos = new Dictionary<T_Code, int>
|
||||
{
|
||||
{ T_Code.Ipb, 1 },
|
||||
|
|
@ -991,38 +763,17 @@ namespace OperationControl.ViewModels
|
|||
{ T_Code.Sen, 7 }
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 10) Limpeza / ordenação dos eventos
|
||||
// ============================================================
|
||||
|
||||
var eventosOrdenados =
|
||||
eventos
|
||||
.Where(x =>
|
||||
!string.IsNullOrWhiteSpace(x.Titulo) &&
|
||||
!string.IsNullOrWhiteSpace(x.Descricao))
|
||||
.GroupBy(x => new
|
||||
{
|
||||
x.Fonte,
|
||||
x.Titulo,
|
||||
x.Descricao,
|
||||
x.Prioridade,
|
||||
x.StatusVisual
|
||||
})
|
||||
.Select(g => g.First())
|
||||
.OrderByDescending(x => x.Prioridade)
|
||||
.ThenBy(x =>
|
||||
x.Modulo.HasValue &&
|
||||
ordemModulos.TryGetValue(
|
||||
x.Modulo.Value,
|
||||
out var ordem)
|
||||
? ordem
|
||||
: int.MaxValue)
|
||||
.ThenBy(x => x.Fonte)
|
||||
.ToList();
|
||||
|
||||
// ============================================================
|
||||
// 11) Estado normal
|
||||
// ============================================================
|
||||
// ============================
|
||||
// 6) Remove vazios / repetidos
|
||||
// ============================
|
||||
var eventosOrdenados = eventos
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Titulo) && !string.IsNullOrWhiteSpace(x.Descricao))
|
||||
.GroupBy(x => new { x.Fonte, x.Titulo, x.Descricao, x.Prioridade, x.StatusVisual })
|
||||
.Select(g => g.First())
|
||||
.OrderByDescending(x => x.Prioridade)
|
||||
.ThenBy(x => x.Modulo.HasValue && ordemModulos.ContainsKey(x.Modulo.Value)? ordemModulos[x.Modulo.Value] : int.MaxValue)
|
||||
.ThenBy(x => x.Fonte)
|
||||
.ToList();
|
||||
|
||||
if (!eventosOrdenados.Any())
|
||||
{
|
||||
|
|
@ -1031,163 +782,96 @@ namespace OperationControl.ViewModels
|
|||
Prioridade = 0,
|
||||
Fonte = "Normal",
|
||||
Titulo = "ROVER OPERANDO NORMALMENTE",
|
||||
Descricao =
|
||||
"Trajetória, controle e operação em condição normal.",
|
||||
StatusVisual =
|
||||
obj.StatusRover == StatusModulo.Desconectado
|
||||
? StatusModulo.Desconectado
|
||||
: StatusModulo.Operante
|
||||
Descricao = "Trajetória, controle e operação em condição normal.",
|
||||
StatusVisual = obj.StatusRover == StatusModulo.Desconectado
|
||||
? StatusModulo.Desconectado
|
||||
: StatusModulo.Operante
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 12) Montagem visual
|
||||
// ============================================================
|
||||
|
||||
var principal =
|
||||
eventosOrdenados.First();
|
||||
|
||||
StatusModulo statusVisual =
|
||||
principal.StatusVisual;
|
||||
|
||||
string linha1 =
|
||||
principal.Titulo;
|
||||
|
||||
// ============================
|
||||
// 7) Monta visual final
|
||||
// ============================
|
||||
StatusModulo statusVisual;
|
||||
string linha1;
|
||||
string linha2;
|
||||
string badge;
|
||||
string resumo;
|
||||
|
||||
// Se perdeu comunicação, não exibimos diagnóstico antigo
|
||||
// recebido antes da queda junto com a perda de comunicação.
|
||||
if (principal.Fonte == "Comunicacao")
|
||||
if (eventosOrdenados.Any())
|
||||
{
|
||||
linha2 = principal.Descricao;
|
||||
var principal = eventosOrdenados.First();
|
||||
|
||||
statusVisual = principal.StatusVisual;
|
||||
linha1 = principal.Titulo;
|
||||
|
||||
// Pega até 3 descrições distintas, por prioridade
|
||||
var descricoes = eventosOrdenados
|
||||
.Select(x => x.Descricao?.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.Take(3)
|
||||
.ToList();
|
||||
|
||||
linha2 = string.Join(" • ", descricoes);
|
||||
|
||||
// Badge curto
|
||||
badge = principal.Fonte switch
|
||||
{
|
||||
"Comunicacao" => "DESCONECTADO",
|
||||
"Emergencia" => "EMERGÊNCIA",
|
||||
"Pausa" => "PAUSADO",
|
||||
"Trajetoria" => "TRAJETÓRIA",
|
||||
"Controle" => "CONTROLE",
|
||||
"ModuloCritico" => "ALERTA",
|
||||
_ => statusVisual.ToString().ToUpper()
|
||||
};
|
||||
|
||||
// Resumo curto
|
||||
resumo = principal.Fonte switch
|
||||
{
|
||||
"Comunicacao" => "Sem telemetria",
|
||||
"Emergencia" => "Parada imediata",
|
||||
"Pausa" => "Aguardando retomada",
|
||||
"Trajetoria" => "Operação bloqueada",
|
||||
"Controle" => "Controle bloqueado",
|
||||
"ModuloCritico" => "Atenção operacional",
|
||||
_ => (obj.Operacao?.Status ?? StatusOperacao.NaoIniciado).ToString()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var descricoes =
|
||||
eventosOrdenados
|
||||
.Select(x => x.Descricao?.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct()
|
||||
.Take(3)
|
||||
.ToList();
|
||||
statusVisual = obj.StatusRover == StatusModulo.Desconectado
|
||||
? StatusModulo.Desconectado
|
||||
: StatusModulo.Operante;
|
||||
|
||||
linha2 =
|
||||
string.Join(" • ", descricoes);
|
||||
linha1 = "ROVER OPERANDO NORMALMENTE";
|
||||
linha2 = "Trajetória, controle e operação em condição normal.";
|
||||
badge = statusVisual.ToString().ToUpper();
|
||||
resumo = (obj.Operacao?.Status ?? StatusOperacao.NaoIniciado).ToString();
|
||||
}
|
||||
|
||||
string badge =
|
||||
principal.Fonte switch
|
||||
{
|
||||
"Comunicacao" => "DESCONECTADO",
|
||||
"NCP" => "DESCONECTADO",
|
||||
"Emergencia" => "EMERGÊNCIA",
|
||||
"Pausa" => "PAUSADO",
|
||||
"Operacao" => "BLOQUEADO",
|
||||
"Controle" => "CONTROLE",
|
||||
"Trajetoria" => "TRAJETÓRIA",
|
||||
"StatusOperacao"
|
||||
=> obj.Operacao?.Status
|
||||
.ToString()
|
||||
.ToUpper() ?? "OPERAÇÃO",
|
||||
"ModuloCritico" => "ALERTA",
|
||||
"Normal" => statusVisual
|
||||
.ToString()
|
||||
.ToUpper(),
|
||||
_ => statusVisual
|
||||
.ToString()
|
||||
.ToUpper()
|
||||
};
|
||||
|
||||
string resumo =
|
||||
principal.Fonte switch
|
||||
{
|
||||
"Comunicacao" =>
|
||||
"Sem telemetria",
|
||||
|
||||
"NCP" =>
|
||||
"Núcleo central desconectado",
|
||||
|
||||
"Emergencia" =>
|
||||
"Parada imediata",
|
||||
|
||||
"Pausa" =>
|
||||
"Aguardando retomada",
|
||||
|
||||
"Operacao" =>
|
||||
"Aguardando liberação",
|
||||
|
||||
"Controle" =>
|
||||
"Controle bloqueado",
|
||||
|
||||
"Trajetoria" =>
|
||||
"Trajetória bloqueada",
|
||||
|
||||
"StatusOperacao" =>
|
||||
NormalizarTexto(
|
||||
obj.Operacao?.MotivoStatusOperacao)
|
||||
?? obj.Operacao?.Status.ToString()
|
||||
?? "Estado operacional",
|
||||
|
||||
"ModuloCritico" =>
|
||||
"Atenção operacional",
|
||||
|
||||
"Normal" =>
|
||||
obj.Operacao?.Status.ToString()
|
||||
?? StatusOperacao.NaoIniciado.ToString(),
|
||||
|
||||
_ =>
|
||||
obj.Operacao?.Status.ToString()
|
||||
?? StatusOperacao.NaoIniciado.ToString()
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 13) Histórico
|
||||
// ============================================================
|
||||
|
||||
_viewOperacaoCenter?
|
||||
.Historico?
|
||||
._vm?
|
||||
.CompararERegistrarMudancasEventos(
|
||||
rover.RoverId,
|
||||
eventosOrdenados);
|
||||
|
||||
// ============================================================
|
||||
// 14) Segurança visual
|
||||
// ============================================================
|
||||
|
||||
if (string.IsNullOrWhiteSpace(linha1))
|
||||
linha1 = "ESTADO DO ROVER";
|
||||
_viewOperacaoCenter?.Historico?._vm?.CompararERegistrarMudancasEventos(rover.RoverId, eventosOrdenados);
|
||||
|
||||
// Segurança extra: evita linha vazia
|
||||
if (string.IsNullOrWhiteSpace(linha2))
|
||||
linha2 = "Sem detalhes adicionais.";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(badge))
|
||||
badge = "STATUS";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(resumo))
|
||||
resumo =
|
||||
obj.Operacao?.Status.ToString()
|
||||
?? StatusOperacao.NaoIniciado.ToString();
|
||||
|
||||
// ============================================================
|
||||
// 15) Atualização da UI
|
||||
// ============================================================
|
||||
|
||||
System.Windows.Application.Current.Dispatcher.BeginInvoke(
|
||||
new Action(() =>
|
||||
{
|
||||
_viewOperacaoTop?._vm?.AtualizarStatus(
|
||||
statusVisual,
|
||||
rover.RoverId,
|
||||
obj.Operacao?.Status
|
||||
?? StatusOperacao.NaoIniciado,
|
||||
linha1,
|
||||
linha2,
|
||||
badge,
|
||||
resumo
|
||||
);
|
||||
})
|
||||
);
|
||||
// ============================
|
||||
// 8) Atualiza UI
|
||||
// ============================
|
||||
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
_viewOperacaoTop?._vm?.AtualizarStatus(
|
||||
statusVisual,
|
||||
rover.RoverId,
|
||||
obj?.Operacao?.Status ?? StatusOperacao.NaoIniciado,
|
||||
linha1,
|
||||
linha2,
|
||||
badge,
|
||||
resumo
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
public void AtualizarBarraSuperior(StatusModulo status, string titulo, string linha1, string linha2, string status_str, string resumo)
|
||||
|
|
|
|||
Loading…
Reference in New Issue