Compare commits
2 Commits
00c9af952a
...
b737b11a59
| Author | SHA1 | Date |
|---|---|---|
|
|
b737b11a59 | |
|
|
9fcd2420b9 |
|
|
@ -1,4 +1,4 @@
|
||||||
using AgroBase.Models;
|
using AgroBase.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
|
@ -52,6 +52,8 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
|
|
||||||
private void CarregarDadosTela()
|
private void CarregarDadosTela()
|
||||||
{
|
{
|
||||||
|
_carregandoDados = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var parametros = op?.Parametros?.Controle;
|
var parametros = op?.Parametros?.Controle;
|
||||||
|
|
@ -97,9 +99,12 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
|
|
||||||
parametros.AtuAgitadorModo = seletorModoAgitador.SelectedMode;
|
parametros.AtuAgitadorModo = seletorModoAgitador.SelectedMode;
|
||||||
|
|
||||||
|
if (agitador != null)
|
||||||
|
{
|
||||||
agitador.Kp = (double)paramKp.Value;
|
agitador.Kp = (double)paramKp.Value;
|
||||||
agitador.Ki = (double)paramKi.Value;
|
agitador.Ki = (double)paramKi.Value;
|
||||||
agitador.Kd = (double)paramKd.Value;
|
agitador.Kd = (double)paramKd.Value;
|
||||||
|
}
|
||||||
|
|
||||||
parametros.AtuDuracaoAtuacao = (int)paramDuracaoAtuacao.Value;
|
parametros.AtuDuracaoAtuacao = (int)paramDuracaoAtuacao.Value;
|
||||||
parametros.AtuPressaoLinha = (double)paramPressaoLinha.Value;
|
parametros.AtuPressaoLinha = (double)paramPressaoLinha.Value;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AgroBase.Forms.IHM.Controls;
|
using AgroBase.Forms.IHM.Controls;
|
||||||
using AgroBase.Models;
|
using AgroBase.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -14,6 +14,7 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
|
|
||||||
private bool _carregandoDados;
|
private bool _carregandoDados;
|
||||||
private bool _mapaExistenteCarregado;
|
private bool _mapaExistenteCarregado;
|
||||||
|
private bool _hidratandoMapaExistente;
|
||||||
|
|
||||||
public ucParametrosMapa()
|
public ucParametrosMapa()
|
||||||
{
|
{
|
||||||
|
|
@ -56,8 +57,12 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
if (mesmaInstanciaMapa && !recarregarMapa)
|
if (mesmaInstanciaMapa && !recarregarMapa)
|
||||||
{
|
{
|
||||||
_mapaExistenteCarregado = true;
|
_mapaExistenteCarregado = true;
|
||||||
lblMapaPlaceholder.Visible = _mapa.mapaService?.DadosMapa == null;
|
lblMapaPlaceholder.Visible =
|
||||||
SincronizarParametrosComMapa();
|
(_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);
|
||||||
AtualizarResumo();
|
AtualizarResumo();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -72,17 +77,28 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
lblMapaPlaceholder.Visible = _mapa?.mapaService?.DadosMapa == null;
|
MapaFeatureCollectionModel mapaRuntime =
|
||||||
|
_mapa?.mapaService?.DadosMapa;
|
||||||
|
|
||||||
SincronizarParametrosComMapa();
|
lblMapaPlaceholder.Visible =
|
||||||
AtualizarResumo();
|
(mapaRuntime ?? op?.Parametros?.Mapa) == null;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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)
|
if (!_mapaExistenteCarregado && op?.Parametros?.Mapa != null)
|
||||||
{
|
{
|
||||||
_mapaExistenteCarregado = true;
|
_mapaExistenteCarregado = true;
|
||||||
|
AtualizarResumo();
|
||||||
CarregarMapaExistenteAsync();
|
CarregarMapaExistenteAsync();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SincronizarParametrosComMapa(permitirSelecaoVazia: false);
|
||||||
|
AtualizarResumo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -92,28 +108,57 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
|
|
||||||
private async void CarregarMapaExistenteAsync()
|
private async void CarregarMapaExistenteAsync()
|
||||||
{
|
{
|
||||||
|
if (_hidratandoMapaExistente)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_hidratandoMapaExistente = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_mapa == null || op?.Parametros?.Mapa == null)
|
if (_mapa == null || op?.Parametros?.Mapa == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
List<string> ruasSelecionadas = op.Parametros.RuasPercorrer != null ? new List<string>(op.Parametros.RuasPercorrer) : new List<string>();
|
/*
|
||||||
|
* 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>();
|
||||||
|
|
||||||
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;
|
_mapa.TipoMapa = op.Parametros.TipoMapa;
|
||||||
|
|
||||||
await _mapa.DefinirRuasSelecionadasAsync(op, ruasSelecionadas, false);
|
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);
|
||||||
|
|
||||||
lblMapaPlaceholder.Visible = false;
|
lblMapaPlaceholder.Visible = false;
|
||||||
|
|
||||||
AtualizarResumo();
|
AtualizarResumo();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Variaveis.MostrarLog("[ucParametrosMapa.CarregarMapaExistente] " + ex.Message);
|
Variaveis.MostrarLog(
|
||||||
|
"[ucParametrosMapa.CarregarMapaExistente] " + ex.Message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_hidratandoMapaExistente = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +181,9 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
if (IsDisposed || Disposing)
|
if (IsDisposed || Disposing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (_hidratandoMapaExistente)
|
||||||
|
return;
|
||||||
|
|
||||||
if (InvokeRequired)
|
if (InvokeRequired)
|
||||||
{
|
{
|
||||||
BeginInvoke(new Action(
|
BeginInvoke(new Action(
|
||||||
|
|
@ -150,13 +198,13 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
|
|
||||||
lblMapaPlaceholder.Visible = false;
|
lblMapaPlaceholder.Visible = false;
|
||||||
|
|
||||||
SincronizarParametrosComMapa();
|
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
|
||||||
AtualizarResumo();
|
AtualizarResumo();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Mapa_RuasSelecionadasAlteradas(object sender, EventArgs e)
|
private void Mapa_RuasSelecionadasAlteradas(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_carregandoDados)
|
if (_carregandoDados || _hidratandoMapaExistente)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (IsDisposed || Disposing)
|
if (IsDisposed || Disposing)
|
||||||
|
|
@ -174,27 +222,52 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SincronizarParametrosComMapa();
|
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
|
||||||
AtualizarResumo();
|
AtualizarResumo();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SincronizarParametrosComMapa()
|
private void SincronizarParametrosComMapa(bool permitirSelecaoVazia)
|
||||||
{
|
{
|
||||||
if (op?.Parametros == null || _mapa == null)
|
if (op?.Parametros == null || _mapa == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
op.Parametros.Mapa = _mapa.mapaService?.DadosMapa;
|
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.TipoMapa = _mapa.TipoMapa;
|
op.Parametros.TipoMapa = _mapa.TipoMapa;
|
||||||
op.Parametros.RuasPercorrer = _mapa.RuasPercorrer != null ? _mapa.RuasPercorrer.ToList() : new List<string>();
|
|
||||||
|
List<string> ruasRuntime =
|
||||||
|
_mapa.RuasPercorrer != null
|
||||||
|
? _mapa.RuasPercorrer.ToList()
|
||||||
|
: new List<string>();
|
||||||
|
|
||||||
|
if (permitirSelecaoVazia || ruasRuntime.Count > 0)
|
||||||
|
op.Parametros.RuasPercorrer = ruasRuntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AtualizarResumo()
|
private void AtualizarResumo()
|
||||||
{
|
{
|
||||||
MapaFeatureCollectionModel dadosMapa = _mapa?.mapaService?.DadosMapa;
|
MapaFeatureCollectionModel dadosMapa =
|
||||||
List<MapaFeatureModel> features = dadosMapa?.features ?? new List<MapaFeatureModel>();
|
_mapa?.mapaService?.DadosMapa ??
|
||||||
List<string> selecionadas = _mapa?.RuasPercorrer ?? new List<string>();
|
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>());
|
||||||
int quantidadeTotal = features.Count;
|
int quantidadeTotal = features.Count;
|
||||||
int quantidadeSelecionada = selecionadas.Count;
|
int quantidadeSelecionada = selecionadas.Count;
|
||||||
double distanciaTotalMapa = features
|
double distanciaTotalMapa = features
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AgroBase.Forms.IHM.Controls;
|
using AgroBase.Forms.IHM.Controls;
|
||||||
using AgroBase.Models;
|
using AgroBase.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -193,6 +193,8 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
pnlAlertaAutonomia.Visible = false;
|
pnlAlertaAutonomia.Visible = false;
|
||||||
pnlStatusAutonomia.Visible = true;
|
pnlStatusAutonomia.Visible = true;
|
||||||
|
|
||||||
|
btnLiberarSupervisao.Enabled = false;
|
||||||
|
|
||||||
pnlStatusAutonomia.BringToFront();
|
pnlStatusAutonomia.BringToFront();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,6 +206,36 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
pnlAlertaAutonomia.BringToFront();
|
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()
|
private int ObterIndiceCorredorAvaliado()
|
||||||
{
|
{
|
||||||
int indiceAtual =
|
int indiceAtual =
|
||||||
|
|
@ -227,8 +259,44 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
|
|
||||||
private void btnLiberarSupervisao_Click(object sender, EventArgs e)
|
private void btnLiberarSupervisao_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
var autonomia = op?.Trajetoria?.AutonomiaCorredor;
|
||||||
|
|
||||||
|
if (!PodeLiberarSupervisao(autonomia))
|
||||||
|
return;
|
||||||
|
|
||||||
int idxCorredor = ObterIndiceCorredorAvaliado();
|
int idxCorredor = ObterIndiceCorredorAvaliado();
|
||||||
op?.Trajetoria?.AtualizarDadosAutonomiaCorredor(idxCorredor: idxCorredor, bat_liberada: true, herb_liberado: true);
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
AtualizarResultadoAutonomia(op?.Trajetoria?.AutonomiaCorredor);
|
AtualizarResultadoAutonomia(op?.Trajetoria?.AutonomiaCorredor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,13 +535,21 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
|
|
||||||
bool operacaoIniciada = op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false;
|
bool operacaoIniciada = op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false;
|
||||||
|
|
||||||
bool aguardandoLiberacao = autonomia.Status == AutonomiaCorredorStatus.AguardandoLiberacaoManual && !autonomia.Liberado;
|
bool podeLiberarSupervisao = PodeLiberarSupervisao(autonomia);
|
||||||
|
|
||||||
if (operacaoIniciada && aguardandoLiberacao)
|
btnLiberarSupervisao.Enabled =
|
||||||
|
operacaoIniciada &&
|
||||||
|
podeLiberarSupervisao;
|
||||||
|
|
||||||
|
if (operacaoIniciada && podeLiberarSupervisao)
|
||||||
{
|
{
|
||||||
AtualizarTextoAlertaAutonomia(autonomia);
|
AtualizarTextoAlertaAutonomia(autonomia);
|
||||||
|
|
||||||
MostrarAlertaAutonomia();
|
MostrarAlertaAutonomia();
|
||||||
|
|
||||||
|
// MostrarStatusAutonomia() desabilita o botão, então restauramos
|
||||||
|
// explicitamente o estado após trazer o painel de alerta.
|
||||||
|
btnLiberarSupervisao.Enabled = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,11 +559,13 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
private void AtualizarTextoAlertaAutonomia(AutonomiaCorredorModel autonomia)
|
private void AtualizarTextoAlertaAutonomia(AutonomiaCorredorModel autonomia)
|
||||||
{
|
{
|
||||||
bool bateriaBloqueando =
|
bool bateriaBloqueando =
|
||||||
|
autonomia.DadosBateriaValidos &&
|
||||||
!autonomia.BateriaSuficiente &&
|
!autonomia.BateriaSuficiente &&
|
||||||
!autonomia.BateriaOverrideAplicado;
|
!autonomia.BateriaOverrideAplicado;
|
||||||
|
|
||||||
bool herbicidaBloqueando =
|
bool herbicidaBloqueando =
|
||||||
autonomia.PulverizacaoExigida &&
|
autonomia.PulverizacaoExigida &&
|
||||||
|
autonomia.DadosHerbicidaValidos &&
|
||||||
!autonomia.HerbicidaSuficiente &&
|
!autonomia.HerbicidaSuficiente &&
|
||||||
!autonomia.HerbicidaOverrideAplicado;
|
!autonomia.HerbicidaOverrideAplicado;
|
||||||
|
|
||||||
|
|
@ -644,10 +722,7 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
lblStatusAvaliacao.Text = !string.IsNullOrWhiteSpace(autonomia.Motivo) ? autonomia.Motivo : "Autonomia atualizada";
|
lblStatusAvaliacao.Text = !string.IsNullOrWhiteSpace(autonomia.Motivo) ? autonomia.Motivo : "Autonomia atualizada";
|
||||||
lblStatusAvaliacao.ForeColor = autonomia.Liberado ? corCorredor : HmiTheme.TextMuted;
|
lblStatusAvaliacao.ForeColor = autonomia.Liberado ? corCorredor : HmiTheme.TextMuted;
|
||||||
|
|
||||||
if (autonomia.Liberado)
|
AtualizarPainelAutonomia(autonomia);
|
||||||
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.Forms.IHM.Operacao.Parametros;
|
||||||
using AgroBase.Models;
|
using AgroBase.Models;
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -155,7 +155,25 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
if (operacaoParametrizando == null)
|
if (operacaoParametrizando == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (operacaoParametrizando.Parametros.Modo == ModoOperacao.MapaGPS && (operacaoParametrizando.Mapa.RuasPercorrer?.Count ?? 0) == 0)
|
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)
|
||||||
|
{
|
||||||
|
int qtdRuas =
|
||||||
|
operacaoParametrizando.Parametros?.RuasPercorrer?.Count
|
||||||
|
?? operacaoParametrizando.Mapa?.RuasPercorrer?.Count
|
||||||
|
?? 0;
|
||||||
|
|
||||||
|
if (qtdRuas == 0)
|
||||||
{
|
{
|
||||||
FuncoesIHM.MostrarAlerta(
|
FuncoesIHM.MostrarAlerta(
|
||||||
"Trajetória não parametrizada",
|
"Trajetória não parametrizada",
|
||||||
|
|
@ -164,9 +182,9 @@ namespace AgroBase.Forms.IHM.Operacao
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var op = Variaveis.OperacaoEmAndamento;
|
if (operacaoIniciada)
|
||||||
if (op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false)
|
|
||||||
op?.AtualizarParametrosParciais(operacaoParametrizando.Parametros, op);
|
op?.AtualizarParametrosParciais(operacaoParametrizando.Parametros, op);
|
||||||
else
|
else
|
||||||
OperacaoModel.CarregarParamerosOperacaoBase(operacaoParametrizando.Parametros);
|
OperacaoModel.CarregarParamerosOperacaoBase(operacaoParametrizando.Parametros);
|
||||||
|
|
|
||||||
|
|
@ -772,7 +772,7 @@ namespace AgroBase.Forms
|
||||||
lblDistProx.Text =
|
lblDistProx.Text =
|
||||||
$"Distância Próximo Ponto: {(proximoPonto?.DistanciaAtual ?? 0):F2} m";
|
$"Distância Próximo Ponto: {(proximoPonto?.DistanciaAtual ?? 0):F2} m";
|
||||||
lblDistAnt.Text =
|
lblDistAnt.Text =
|
||||||
$"Dist Esquerda: {dadosTrajetoria.DistanciaEsquerda * 100:F0} cm | Dist Direita: {dadosTrajetoria.DistanciaDireita * 100:F0} cm";
|
$"Esq: {dadosTrajetoria.DistanciaEsquerda * 100:F0} cm | Dir: {dadosTrajetoria.DistanciaDireita * 100:F0} cm | Err: {(dadosTrajetoria.DistanciaDireita - dadosTrajetoria.DistanciaEsquerda) * 100:F0} cm";
|
||||||
lblTempoRestante.Text =
|
lblTempoRestante.Text =
|
||||||
$"Tempo Restante: {dadosTrajetoria.TempoEstimadoRestante}";
|
$"Tempo Restante: {dadosTrajetoria.TempoEstimadoRestante}";
|
||||||
lblTempoOperacao.Text =
|
lblTempoOperacao.Text =
|
||||||
|
|
@ -1446,6 +1446,15 @@ namespace AgroBase.Forms
|
||||||
|
|
||||||
Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1);
|
Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1);
|
||||||
AtualizarDadosTela();
|
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)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -2599,12 +2599,24 @@ namespace AgroBase.Models.Modules
|
||||||
if (op?.DispAtu?.Dados == null)
|
if (op?.DispAtu?.Dados == null)
|
||||||
return tasks;
|
return tasks;
|
||||||
|
|
||||||
tasks.Add(ExecutarCalibragemProtegidaAsync(forcar));
|
// 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));
|
||||||
|
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExecutarCalibragemProtegidaAsync(bool forcar)
|
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)
|
||||||
{
|
{
|
||||||
var op = Variaveis.OperacaoEmAndamento;
|
var op = Variaveis.OperacaoEmAndamento;
|
||||||
|
|
||||||
|
|
@ -2637,11 +2649,15 @@ namespace AgroBase.Models.Modules
|
||||||
|
|
||||||
op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v));
|
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)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Calibragem cancelada: {ex.Message}");
|
Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Calibragem cancelada: {ex.Message}");
|
||||||
op?.Sensoriamento?.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Calibragem do módulo {Modulo_ID} cancelada: {ex.Message}");
|
op?.Sensoriamento?.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Calibragem do módulo {Modulo_ID} cancelada: {ex.Message}");
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AgroBase.Comum;
|
using AgroBase.Comum;
|
||||||
using AgroBase.Forms;
|
using AgroBase.Forms;
|
||||||
using AgroBase.Forms.Operacoes;
|
using AgroBase.Forms.Operacoes;
|
||||||
using AgroBase.Models.Modules;
|
using AgroBase.Models.Modules;
|
||||||
|
|
@ -349,6 +349,12 @@ namespace AgroBase.Models
|
||||||
private static string _discoverySessionId = Guid.NewGuid().ToString("N");
|
private static string _discoverySessionId = Guid.NewGuid().ToString("N");
|
||||||
private static long _telemetrySequence = 0;
|
private static long _telemetrySequence = 0;
|
||||||
private readonly SemaphoreSlim _calibragemGate = new SemaphoreSlim(1, 1);
|
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
|
public bool CalibrandoRuntime
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -361,6 +367,12 @@ namespace AgroBase.Models
|
||||||
private readonly TimeSpan _cooldownCalibragem = TimeSpan.FromSeconds(30);
|
private readonly TimeSpan _cooldownCalibragem = TimeSpan.FromSeconds(30);
|
||||||
private bool PodeTentarCalibragemAutomatica()
|
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;
|
return DateTime.Now - _ultimaTentativaCalibragem > _cooldownCalibragem;
|
||||||
}
|
}
|
||||||
private void RegistrarTentativaCalibragem()
|
private void RegistrarTentativaCalibragem()
|
||||||
|
|
@ -1395,6 +1407,11 @@ namespace AgroBase.Models
|
||||||
|
|
||||||
op.Sensoriamento.UltimoRegistroLog = agora;
|
op.Sensoriamento.UltimoRegistroLog = agora;
|
||||||
op.idxLog = 0;
|
op.idxLog = 0;
|
||||||
|
|
||||||
|
// Cada nova operação ganha um ciclo de calibração novo.
|
||||||
|
op._calibragemInicialConcluida = false;
|
||||||
|
op._ultimaTentativaCalibragem = DateTime.MinValue;
|
||||||
|
|
||||||
op.IniciarTimerLogsOperacao();
|
op.IniciarTimerLogsOperacao();
|
||||||
|
|
||||||
RedisService.Publish(CmdKey.ManagerWorkerRx, JsonConvert.SerializeObject(new { cmd = ManagerWorkerCommandType.IniciarMPC }));
|
RedisService.Publish(CmdKey.ManagerWorkerRx, JsonConvert.SerializeObject(new { cmd = ManagerWorkerCommandType.IniciarMPC }));
|
||||||
|
|
@ -1665,6 +1682,12 @@ namespace AgroBase.Models
|
||||||
if (!await _calibragemGate.WaitAsync(0))
|
if (!await _calibragemGate.WaitAsync(0))
|
||||||
return;
|
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;
|
TipoMovimentoDirecional tipoControle = op.Controle.TipoMovimento;
|
||||||
_pausaAntesCalibragem = op.Sensoriamento.Operacao.Pausa;
|
_pausaAntesCalibragem = op.Sensoriamento.Operacao.Pausa;
|
||||||
bool entrouEmCalibragem = false;
|
bool entrouEmCalibragem = false;
|
||||||
|
|
@ -1771,9 +1794,16 @@ namespace AgroBase.Models
|
||||||
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem dos módulos iniciada...");
|
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem dos módulos iniciada...");
|
||||||
|
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
|
Task<bool> taskCalibragemAtu = null;
|
||||||
|
|
||||||
if (calibragemAtuNecessaria)
|
if (calibragemAtuNecessaria)
|
||||||
tasks.AddRange(dispAtu.Dados.RealizarCalibragemAsync(forcar));
|
{
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
if (calibragemDirNecessaria)
|
if (calibragemDirNecessaria)
|
||||||
tasks.AddRange(dispMvd.Dados.RealizarCalibragemDirAsync(forcar));
|
tasks.AddRange(dispMvd.Dados.RealizarCalibragemDirAsync(forcar));
|
||||||
|
|
@ -1799,12 +1829,56 @@ namespace AgroBase.Models
|
||||||
{
|
{
|
||||||
await allTasks;
|
await allTasks;
|
||||||
|
|
||||||
|
bool atuSucesso =
|
||||||
|
taskCalibragemAtu == null ||
|
||||||
|
(taskCalibragemAtu.Status == TaskStatus.RanToCompletion && taskCalibragemAtu.Result);
|
||||||
|
|
||||||
|
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(
|
Variaveis.MostrarLog(
|
||||||
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
"[OperacaoModel.RealizarCalibragemInicialAsync] " + mensagem
|
||||||
"Tasks de calibragem dos módulos concluídas."
|
|
||||||
);
|
);
|
||||||
|
|
||||||
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem finalizada.");
|
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."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1877,6 +1951,17 @@ 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
|
try
|
||||||
{
|
{
|
||||||
_calibragemGate.Release();
|
_calibragemGate.Release();
|
||||||
|
|
@ -3291,10 +3376,9 @@ namespace AgroBase.Models
|
||||||
op.RegistrarTentativaCalibragem();
|
op.RegistrarTentativaCalibragem();
|
||||||
_ = Task.Run(async () => await op.RealizarCalibragemInicialAsync(false));
|
_ = Task.Run(async () => await op.RealizarCalibragemInicialAsync(false));
|
||||||
}
|
}
|
||||||
if (op.CalibrandoRuntime && op.TodosModulosMandatoriosCalibrados())
|
|
||||||
{
|
// O estado Calibrando pertence exclusivamente à rotina de calibração.
|
||||||
op.DefinirEstadoCalibragemRuntime(false);
|
// O timer não deve liberar a operação antes das Tasks terminarem.
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4140,6 +4224,9 @@ namespace AgroBase.Models
|
||||||
OperacaoLiberacaoDebug = RedisService.GetField<JObject>(CtxKey.DadosOperacao, "operacao_liberacao_debug"),
|
OperacaoLiberacaoDebug = RedisService.GetField<JObject>(CtxKey.DadosOperacao, "operacao_liberacao_debug"),
|
||||||
StatusOperacaoAtual = (StatusOperacao)RedisService.GetField<int>(CtxKey.DadosOperacao, "status", 0),
|
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>()),
|
ModulosMandatorios = new List<OperacaoModulosMandatoriosModel>(op.Parametros.ModulosMandatorios ?? new List<OperacaoModulosMandatoriosModel>()),
|
||||||
ParametrosMandatorios = new List<OperacaoParametrosMandatoriosModel>(op.Parametros.ParametrosMandatorios ?? new List<OperacaoParametrosMandatoriosModel>()),
|
ParametrosMandatorios = new List<OperacaoParametrosMandatoriosModel>(op.Parametros.ParametrosMandatorios ?? new List<OperacaoParametrosMandatoriosModel>()),
|
||||||
DispositivosMapeados = new List<DispositivoDetalhesModel>(SerialService.DispositivosMapeados),
|
DispositivosMapeados = new List<DispositivoDetalhesModel>(SerialService.DispositivosMapeados),
|
||||||
|
|
@ -5375,6 +5462,8 @@ namespace AgroBase.Models
|
||||||
public StatusOperacao StatusOperacaoAnterior { get; set; }
|
public StatusOperacao StatusOperacaoAnterior { get; set; }
|
||||||
public string ErroOperacaoLiberada { get; set; }
|
public string ErroOperacaoLiberada { get; set; }
|
||||||
public JObject OperacaoLiberacaoDebug { get; set; }
|
public JObject OperacaoLiberacaoDebug { get; set; }
|
||||||
|
public string MotivoStatusOperacao { get; set; }
|
||||||
|
public string UltimoMotivoParada { get; set; }
|
||||||
public bool OperacaoConfigurada { get; set; }
|
public bool OperacaoConfigurada { get; set; }
|
||||||
public bool OperacaoLiberada { get; set; }
|
public bool OperacaoLiberada { get; set; }
|
||||||
public bool OperacaoIniciada { get; set; }
|
public bool OperacaoIniciada { get; set; }
|
||||||
|
|
@ -5439,6 +5528,8 @@ namespace AgroBase.Models
|
||||||
Status = Status,
|
Status = Status,
|
||||||
ErroOperacaoLiberada = ErroOperacaoLiberada,
|
ErroOperacaoLiberada = ErroOperacaoLiberada,
|
||||||
OperacaoLiberacaoDebug = OperacaoLiberacaoDebug,
|
OperacaoLiberacaoDebug = OperacaoLiberacaoDebug,
|
||||||
|
MotivoStatusOperacao = MotivoStatusOperacao,
|
||||||
|
UltimoMotivoParada = UltimoMotivoParada,
|
||||||
OperacaoConfigurada = OperacaoConfigurada,
|
OperacaoConfigurada = OperacaoConfigurada,
|
||||||
OperacaoLiberada = OperacaoLiberada,
|
OperacaoLiberada = OperacaoLiberada,
|
||||||
OperacaoIniciada = OperacaoIniciada,
|
OperacaoIniciada = OperacaoIniciada,
|
||||||
|
|
|
||||||
|
|
@ -712,6 +712,8 @@ namespace AgroBase.Models.Operacoes
|
||||||
Iniciada = op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false,
|
Iniciada = op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false,
|
||||||
Liberada = op?.Sensoriamento?.Operacao?.OperacaoLiberada ?? false,
|
Liberada = op?.Sensoriamento?.Operacao?.OperacaoLiberada ?? false,
|
||||||
Status = op?.Sensoriamento?.Operacao?.StatusOperacaoAtual ?? StatusOperacao.Erro,
|
Status = op?.Sensoriamento?.Operacao?.StatusOperacaoAtual ?? StatusOperacao.Erro,
|
||||||
|
UltimoMotivoParada = op?.Sensoriamento?.Operacao?.UltimoMotivoParada,
|
||||||
|
MotivoStatusOperacao = op?.Sensoriamento?.Operacao?.MotivoStatusOperacao,
|
||||||
Modo = parametros?.Modo ?? ModoOperacao.NaoDefinido,
|
Modo = parametros?.Modo ?? ModoOperacao.NaoDefinido,
|
||||||
ImpedimentoErro = op?.Sensoriamento?.Operacao?.ErroOperacaoLiberada,
|
ImpedimentoErro = op?.Sensoriamento?.Operacao?.ErroOperacaoLiberada,
|
||||||
TempoAguardandoSegs = op?.Sensoriamento?.Operacao?.TempoAguardandoSeg ?? 0,
|
TempoAguardandoSegs = op?.Sensoriamento?.Operacao?.TempoAguardandoSeg ?? 0,
|
||||||
|
|
@ -1497,6 +1499,8 @@ namespace AgroBase.Models.Operacoes
|
||||||
public ModoOperacao Modo { get; set; }
|
public ModoOperacao Modo { get; set; }
|
||||||
public StatusOperacao Status { get; set; }
|
public StatusOperacao Status { get; set; }
|
||||||
public string ImpedimentoErro { get; set; }
|
public string ImpedimentoErro { get; set; }
|
||||||
|
public string MotivoStatusOperacao { get; set; }
|
||||||
|
public string UltimoMotivoParada { get; set; }
|
||||||
public bool Liberada { get; set; }
|
public bool Liberada { get; set; }
|
||||||
public bool Iniciada { get; set; }
|
public bool Iniciada { get; set; }
|
||||||
public bool Emergencia { get; set; }
|
public bool Emergencia { get; set; }
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1084,87 +1084,215 @@ namespace AgroBase.Models
|
||||||
return novoPonto;
|
return novoPonto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GPSModel SimularNovaPosicao(TipoMovimentoDirecional tipoMovimento, double anguloControleDeg, double velocidadeMs, double tempoDelta, double anguloAtualDeg, GPSModel pos)
|
// V16 - Simulação 4WS unificada com o MPC Python.
|
||||||
{
|
//
|
||||||
double L = DistanciaEntreEixosCm / 100.0;
|
// 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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
// Defina deltas por modo
|
|
||||||
double dfDeg = 0, drDeg = 0;
|
|
||||||
switch (tipoMovimento)
|
switch (tipoMovimento)
|
||||||
{
|
{
|
||||||
case TipoMovimentoDirecional.RodasDianteiras: dfDeg = anguloControleDeg; drDeg = 0; break;
|
case TipoMovimentoDirecional.RodasDianteiras:
|
||||||
case TipoMovimentoDirecional.RodasTraseiras: dfDeg = 0; drDeg = anguloControleDeg; break;
|
dfDeg = anguloControleDeg;
|
||||||
case TipoMovimentoDirecional.MovimentoArco: dfDeg = anguloControleDeg; drDeg = -anguloControleDeg; break;
|
drDeg = 0.0;
|
||||||
case TipoMovimentoDirecional.MovimentoDiagonal: dfDeg = anguloControleDeg; drDeg = anguloControleDeg; break;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Curvatura geométrica
|
double df = dfDeg * Math.PI / 180.0;
|
||||||
double tf = Math.Tan(dfDeg * Math.PI / 180.0);
|
double dr = drDeg * 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
|
double tf = Math.Tan(df);
|
||||||
if (tipoMovimento == TipoMovimentoDirecional.RodasDianteiras) kappa = tf / L;
|
double tr = Math.Tan(dr);
|
||||||
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)
|
if (Math.Abs(tf) < 1e-12) tf = 0.0;
|
||||||
double Ku = 2.5; // ajuste fino em campo
|
if (Math.Abs(tr) < 1e-12) tr = 0.0;
|
||||||
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);
|
||||||
|
|
||||||
// Integração
|
|
||||||
double theta = anguloAtualDeg * Math.PI / 180.0;
|
|
||||||
double omega = velocidadeMs * kappaEf; // rad/s
|
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;
|
||||||
double dtheta = omega * tempoDelta;
|
double dtheta = omega * tempoDelta;
|
||||||
theta += dtheta;
|
double theta1 = theta0 + dtheta;
|
||||||
|
|
||||||
double heading = theta;
|
double phi0 = theta0 + beta;
|
||||||
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;
|
||||||
double dx = velocidadeMs * tempoDelta * Math.Sin(heading); // Leste(+)
|
double dy;
|
||||||
double dy = velocidadeMs * tempoDelta * Math.Cos(heading); // Norte(+)
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
GPSModel ultimaPosicao = GPSService.historicoPosicao.Peek();
|
GPSModel ultimaPosicao = GPSService.historicoPosicao.Peek();
|
||||||
if (pos == null) return ultimaPosicao;
|
if (pos == null)
|
||||||
|
return ultimaPosicao;
|
||||||
|
|
||||||
// geo
|
// ------------------------------------------------------------
|
||||||
|
// 5) Plano local -> geográfico
|
||||||
|
// ------------------------------------------------------------
|
||||||
double R_earth = GPSUtils.RaioDaTerra;
|
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 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 latitude = pos.LatitudeAnt + dLat;
|
double latitude = pos.LatitudeAnt + dLat;
|
||||||
double longitude = pos.LongitudeAnt + dLon;
|
double longitude = pos.LongitudeAnt + dLon;
|
||||||
|
|
||||||
DateTime agora = DateTime.Now;
|
DateTime agora = DateTime.Now;
|
||||||
double agoraMono = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
|
double agoraMono =
|
||||||
|
Stopwatch.GetTimestamp() /
|
||||||
|
(double)Stopwatch.Frequency;
|
||||||
|
|
||||||
double orientacaoRealDeg = GPSUtils.NormalizarAngulo(theta * 180.0 / Math.PI);
|
double orientacaoRealDeg =
|
||||||
double orientacaoMovimentoDeg = GPSUtils.NormalizarAngulo(heading * 180.0 / Math.PI);
|
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
|
||||||
|
);
|
||||||
|
|
||||||
GPSModel novaPosicao = new GPSModel
|
GPSModel novaPosicao = new GPSModel
|
||||||
{
|
{
|
||||||
Momento = agora,
|
Momento = agora,
|
||||||
DataHora = agora,
|
DataHora = agora,
|
||||||
UltimoComandoRespondido = agora,
|
UltimoComandoRespondido = agora,
|
||||||
|
|
||||||
Lat0 = GPSService.UltimaLeitura.Lat0,
|
Lat0 = GPSService.UltimaLeitura.Lat0,
|
||||||
Lon0 = GPSService.UltimaLeitura.Lon0,
|
Lon0 = GPSService.UltimaLeitura.Lon0,
|
||||||
|
|
||||||
LatitudeAnt = latitude,
|
LatitudeAnt = latitude,
|
||||||
LongitudeAnt = longitude,
|
LongitudeAnt = longitude,
|
||||||
|
|
||||||
OrientacaoReal = orientacaoRealDeg,
|
OrientacaoReal = orientacaoRealDeg,
|
||||||
AnguloCarroDefinido = orientacaoRealDeg,
|
AnguloCarroDefinido = orientacaoRealDeg,
|
||||||
OrientacaoMovimento = orientacaoMovimentoDeg,
|
OrientacaoMovimento = orientacaoMovimentoDeg,
|
||||||
|
|
||||||
TipoOrientacao = "A",
|
TipoOrientacao = "A",
|
||||||
Distancia = velocidadeMs * tempoDelta,
|
Distancia = velocidadeMs * tempoDelta,
|
||||||
Velocidade = velocidadeMs,
|
Velocidade = velocidadeMs,
|
||||||
|
|
||||||
Heartbeat = ultimaPosicao.Heartbeat + 1,
|
Heartbeat = ultimaPosicao.Heartbeat + 1,
|
||||||
|
|
||||||
TimestampOri = ultimaPosicao.TimestampOri.Clone(),
|
TimestampOri = ultimaPosicao.TimestampOri.Clone(),
|
||||||
TimestampPos = ultimaPosicao.TimestampPos.Clone(),
|
TimestampPos = ultimaPosicao.TimestampPos.Clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mantém exatamente o lever-arm já usado no simulador real.
|
||||||
var corrigida =
|
var corrigida =
|
||||||
GPSService.LeverArm.FixLeverArmLatLon_Fast(
|
GPSService.LeverArm.FixLeverArmLatLon_Fast(
|
||||||
novaPosicao.LatitudeAnt,
|
novaPosicao.LatitudeAnt,
|
||||||
|
|
@ -1172,13 +1300,10 @@ namespace AgroBase.Models
|
||||||
novaPosicao.OrientacaoReal,
|
novaPosicao.OrientacaoReal,
|
||||||
5
|
5
|
||||||
);
|
);
|
||||||
|
|
||||||
novaPosicao.Latitude = corrigida.lat;
|
novaPosicao.Latitude = corrigida.lat;
|
||||||
novaPosicao.Longitude = corrigida.lon;
|
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.TimestampOri.valor = agoraMono;
|
||||||
novaPosicao.TimestampPos.valor = agoraMono;
|
novaPosicao.TimestampPos.valor = agoraMono;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -576,10 +576,16 @@ class CameraMultispectral:
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if stream is not None and hasattr(stream, "parar"):
|
if stream is not None:
|
||||||
stream.parar()
|
fechar_stream = getattr(stream, "fechar", None)
|
||||||
except Exception:
|
if not callable(fechar_stream):
|
||||||
pass
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.ultimo_tensor_multispec = None
|
self.ultimo_tensor_multispec = None
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,19 @@ class CameraOak:
|
||||||
self.rodando = False
|
self.rodando = False
|
||||||
self.iniciado = 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:
|
try:
|
||||||
if self._cache_thread is not None and self._cache_thread.is_alive():
|
if self._cache_thread is not None and self._cache_thread.is_alive():
|
||||||
self._cache_thread.join(timeout=1.0)
|
self._cache_thread.join(timeout=1.0)
|
||||||
|
|
@ -282,10 +295,6 @@ class CameraOak:
|
||||||
"frame_valido": False,
|
"frame_valido": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
|
||||||
self.stream = None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _is_erro_fatal_depthai(self, erro):
|
def _is_erro_fatal_depthai(self, erro):
|
||||||
if erro is None:
|
if erro is None:
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,8 @@ class OakFcc3Manager:
|
||||||
"dropped_queue": 0,
|
"dropped_queue": 0,
|
||||||
"last_error": None,
|
"last_error": None,
|
||||||
"last_loop_ms": 0.0,
|
"last_loop_ms": 0.0,
|
||||||
|
"last_packet_age_ms": None,
|
||||||
|
"_last_frame_seen_perf_by_cam": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|
@ -1633,6 +1635,15 @@ class OakFcc3Manager:
|
||||||
|
|
||||||
t0_get = self._cap_now_ms()
|
t0_get = self._cap_now_ms()
|
||||||
msg = q.get()
|
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:
|
if perf is not None:
|
||||||
perf["drain_get_msg_ms"] += self._cap_now_ms() - t0_get
|
perf["drain_get_msg_ms"] += self._cap_now_ms() - t0_get
|
||||||
|
|
||||||
|
|
@ -2343,6 +2354,7 @@ class OakFcc3Manager:
|
||||||
"last_error": None,
|
"last_error": None,
|
||||||
"last_loop_ms": 0.0,
|
"last_loop_ms": 0.0,
|
||||||
"last_packet_age_ms": None,
|
"last_packet_age_ms": None,
|
||||||
|
"_last_frame_seen_perf_by_cam": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _start_async_capture_thread(self):
|
def _start_async_capture_thread(self):
|
||||||
|
|
@ -2417,6 +2429,11 @@ class OakFcc3Manager:
|
||||||
# Importante: esta thread é a única que mexe nas queues/buffers.
|
# Importante: esta thread é a única que mexe nas queues/buffers.
|
||||||
self._drain_queues_to_buffers(perf=perf)
|
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
|
# Em RAW_BRUTO + async, o produtor publica apenas descritores
|
||||||
# ImgFrame da tripleta sincronizada. O consumidor materializa
|
# ImgFrame da tripleta sincronizada. O consumidor materializa
|
||||||
# somente o pacote latest que realmente pegar. Isso elimina
|
# somente o pacote latest que realmente pegar. Isso elimina
|
||||||
|
|
@ -2432,6 +2449,25 @@ class OakFcc3Manager:
|
||||||
)
|
)
|
||||||
|
|
||||||
if synced is None:
|
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.
|
# Dorme curto. Pode testar 0.0005 se quiser reduzir latência.
|
||||||
time.sleep(0.001)
|
time.sleep(0.001)
|
||||||
continue
|
continue
|
||||||
|
|
@ -2503,6 +2539,10 @@ class OakFcc3Manager:
|
||||||
|
|
||||||
self._capture_thread_stats["packets"] += 1
|
self._capture_thread_stats["packets"] += 1
|
||||||
self._capture_thread_stats["last_error"] = None
|
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_thread_stats["last_loop_ms"] = (time.perf_counter() - t_loop0) * 1000.0
|
||||||
|
|
||||||
self._capture_cond.notify_all()
|
self._capture_cond.notify_all()
|
||||||
|
|
@ -2550,6 +2590,20 @@ class OakFcc3Manager:
|
||||||
latest_age_ms = (time.perf_counter() - float(self._latest_packet.get("created_perf_counter", 0.0))) * 1000.0
|
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 {})
|
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({
|
st.update({
|
||||||
"enabled": bool(getattr(self, "async_capture_enabled", True)),
|
"enabled": bool(getattr(self, "async_capture_enabled", True)),
|
||||||
"mode": str(getattr(self, "async_capture_mode", "latest")),
|
"mode": str(getattr(self, "async_capture_mode", "latest")),
|
||||||
|
|
@ -2559,5 +2613,8 @@ class OakFcc3Manager:
|
||||||
"last_consumed_seq": int(getattr(self, "_last_consumed_packet_seq", 0)),
|
"last_consumed_seq": int(getattr(self, "_last_consumed_packet_seq", 0)),
|
||||||
"queue_len": int(len(getattr(self, "_packet_queue", []))),
|
"queue_len": int(len(getattr(self, "_packet_queue", []))),
|
||||||
"latest_age_ms": latest_age_ms,
|
"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
|
return st
|
||||||
|
|
|
||||||
|
|
@ -1572,6 +1572,10 @@ class CameraTcpStreamer:
|
||||||
stream_fps=0.0,
|
stream_fps=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def parar(self):
|
||||||
|
"""Alias de compatibilidade para encerramento explícito."""
|
||||||
|
self.fechar()
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
try:
|
try:
|
||||||
self.fechar()
|
self.fechar()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1651,14 +1651,25 @@ class CameraManager:
|
||||||
threading.Thread(target=loop, daemon=True).start()
|
threading.Thread(target=loop, daemon=True).start()
|
||||||
|
|
||||||
def _iniciar_loop_frame_stream(self, freq=2.0):
|
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():
|
def loop():
|
||||||
while True:
|
while True:
|
||||||
t_wall0 = time.time()
|
t_wall0 = time.time()
|
||||||
t_loop0 = time.perf_counter()
|
t_loop0 = time.perf_counter()
|
||||||
|
stream_on = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.camera is None:
|
camera_atual = self.camera
|
||||||
time.sleep(0.5)
|
if camera_atual is None:
|
||||||
|
time.sleep(0.25)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
|
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
|
||||||
|
|
@ -1673,7 +1684,7 @@ class CameraManager:
|
||||||
frame = self._get_cached_preview_frame(frame_type, copiar=False)
|
frame = self._get_cached_preview_frame(frame_type, copiar=False)
|
||||||
|
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
self.camera.enviar_frame_tcp(frame)
|
camera_atual.enviar_frame_tcp(frame)
|
||||||
|
|
||||||
if self.debug_visual and self._visual_disponivel_para_frame():
|
if self.debug_visual and self._visual_disponivel_para_frame():
|
||||||
self._solicitar_preview(TipoFrameCamera.Debug)
|
self._solicitar_preview(TipoFrameCamera.Debug)
|
||||||
|
|
@ -1692,15 +1703,14 @@ class CameraManager:
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
lat = time.time() - t_wall0
|
lat = time.time() - t_wall0
|
||||||
|
hz = producer_hz if stream_on else 2.0
|
||||||
|
time.sleep(max(0.0, (1.0 / hz) - lat))
|
||||||
|
|
||||||
try:
|
threading.Thread(
|
||||||
freq_atual = float(getattr(self.camera.stream, "_op_fps", freq) or freq)
|
target=loop,
|
||||||
except Exception:
|
daemon=True,
|
||||||
freq_atual = freq
|
name="visual-stream-feed",
|
||||||
|
).start()
|
||||||
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 _iniciar_loop_analise_continua(self, freq=8.0):
|
||||||
def loop():
|
def loop():
|
||||||
|
|
|
||||||
|
|
@ -3231,14 +3231,26 @@ class CameraManager:
|
||||||
threading.Thread(target=loop, daemon=True).start()
|
threading.Thread(target=loop, daemon=True).start()
|
||||||
|
|
||||||
def _iniciar_loop_frame_stream(self, freq=2.0):
|
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():
|
def loop():
|
||||||
while True:
|
while True:
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
t_loop0 = time.perf_counter()
|
t_loop0 = time.perf_counter()
|
||||||
|
stream_on = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.camera is None:
|
camera_atual = self.camera
|
||||||
time.sleep(5.0)
|
if camera_atual is None:
|
||||||
|
time.sleep(0.25)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
|
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
|
||||||
|
|
@ -3248,9 +3260,11 @@ class CameraManager:
|
||||||
frame_type = TipoFrameCamera(
|
frame_type = TipoFrameCamera(
|
||||||
camera_ctx.get("frame_type", TipoFrameCamera.Rgb.value)
|
camera_ctx.get("frame_type", TipoFrameCamera.Rgb.value)
|
||||||
)
|
)
|
||||||
# Stream também é consumidor de cache. Não monta preview.
|
|
||||||
|
# Stream é apenas consumidor do cache pronto.
|
||||||
frame = self._get_cached_preview_frame_ref(frame_type)
|
frame = self._get_cached_preview_frame_ref(frame_type)
|
||||||
self.camera.enviar_frame_tcp(frame)
|
if frame is not None:
|
||||||
|
camera_atual.enviar_frame_tcp(frame)
|
||||||
|
|
||||||
if self.debug_visual:
|
if self.debug_visual:
|
||||||
dbg = self._get_cached_preview_frame_ref(TipoFrameCamera.Debug)
|
dbg = self._get_cached_preview_frame_ref(TipoFrameCamera.Debug)
|
||||||
|
|
@ -3270,15 +3284,14 @@ class CameraManager:
|
||||||
)
|
)
|
||||||
|
|
||||||
lat = time.time() - t0
|
lat = time.time() - t0
|
||||||
|
hz = producer_hz if stream_on else 2.0
|
||||||
|
time.sleep(max(0.0, (1.0 / hz) - lat))
|
||||||
|
|
||||||
try:
|
threading.Thread(
|
||||||
freq_atual = float(getattr(self.camera.stream, "_op_fps", freq) or freq)
|
target=loop,
|
||||||
except Exception:
|
daemon=True,
|
||||||
freq_atual = freq
|
name="weed-stream-feed",
|
||||||
|
).start()
|
||||||
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 _iniciar_loop_analise_continua(self, freq=15.0):
|
||||||
def loop():
|
def loop():
|
||||||
|
|
|
||||||
|
|
@ -537,6 +537,7 @@ namespace OperationControl.ViewModels
|
||||||
public void AtualizarBarraSuperior(OperacaoParametrosModel rover)
|
public void AtualizarBarraSuperior(OperacaoParametrosModel rover)
|
||||||
{
|
{
|
||||||
var obj = rover?.DadosLeitura;
|
var obj = rover?.DadosLeitura;
|
||||||
|
|
||||||
if (rover == null || obj == null)
|
if (rover == null || obj == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -545,12 +546,76 @@ namespace OperationControl.ViewModels
|
||||||
|
|
||||||
var eventos = new List<MotivoTopBarModel>();
|
var eventos = new List<MotivoTopBarModel>();
|
||||||
|
|
||||||
double tempoSemResposta = (DateTime.Now - rover.UltimoContato).TotalSeconds;
|
// ============================================================
|
||||||
bool semComunicacao = !AppShell.Mock && tempoSemResposta >= VariaveisControleOperacao.TempoRoverVivo;
|
// 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;
|
||||||
|
|
||||||
// ============================
|
|
||||||
// 0) Perda de comunicação
|
|
||||||
// ============================
|
|
||||||
if (semComunicacao)
|
if (semComunicacao)
|
||||||
{
|
{
|
||||||
eventos.Add(new MotivoTopBarModel
|
eventos.Add(new MotivoTopBarModel
|
||||||
|
|
@ -558,63 +623,116 @@ namespace OperationControl.ViewModels
|
||||||
Prioridade = 2000,
|
Prioridade = 2000,
|
||||||
Fonte = "Comunicacao",
|
Fonte = "Comunicacao",
|
||||||
Titulo = "PERDA DE COMUNICAÇÃO COM O ROVER",
|
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
|
StatusVisual = StatusModulo.Desconectado
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================================================
|
||||||
// 0) NCP desconectado
|
// 1) NPC / núcleo central
|
||||||
// ============================
|
// ============================================================
|
||||||
if (obj.ModulosSaude?.All(x => (x?.status ?? StatusModulo.Desconectado) == StatusModulo.Desconectado) ?? false)
|
|
||||||
|
bool todosModulosDesconectados =
|
||||||
|
obj.ModulosSaude?.Any() == true &&
|
||||||
|
obj.ModulosSaude.All(x =>
|
||||||
|
(x?.status ?? StatusModulo.Desconectado) ==
|
||||||
|
StatusModulo.Desconectado);
|
||||||
|
|
||||||
|
if (todosModulosDesconectados)
|
||||||
{
|
{
|
||||||
eventos.Add(new MotivoTopBarModel
|
eventos.Add(new MotivoTopBarModel
|
||||||
{
|
{
|
||||||
Prioridade = 1100,
|
Prioridade = 1100,
|
||||||
Fonte = "NCP",
|
Fonte = "NCP",
|
||||||
Titulo = "NÚCLEO CENTRAL DE PROCESSAMENTO DESCONECTADO",
|
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
|
StatusVisual = StatusModulo.Falha
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================================================
|
||||||
// 1) Emergência
|
// 2) Emergência
|
||||||
// ============================
|
// ============================================================
|
||||||
|
|
||||||
if (obj.Operacao?.Emergencia == true)
|
if (obj.Operacao?.Emergencia == true)
|
||||||
{
|
{
|
||||||
|
var motivo =
|
||||||
|
NormalizarTexto(obj.Operacao.MotivoStatusOperacao)
|
||||||
|
?? "Parada de emergência solicitada.";
|
||||||
|
|
||||||
eventos.Add(new MotivoTopBarModel
|
eventos.Add(new MotivoTopBarModel
|
||||||
{
|
{
|
||||||
Prioridade = 1000,
|
Prioridade = 1000,
|
||||||
Fonte = "Emergencia",
|
Fonte = "Emergencia",
|
||||||
Titulo = "OPERAÇÃO BLOQUEADA POR EMERGÊNCIA",
|
Titulo = "OPERAÇÃO BLOQUEADA POR EMERGÊNCIA",
|
||||||
Descricao = "Parada de emergência solicitada.",
|
Descricao = motivo,
|
||||||
StatusVisual = StatusModulo.Falha
|
StatusVisual = StatusModulo.Falha
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================================================
|
||||||
// 2) Pausa
|
// 3) Pausa
|
||||||
// ============================
|
// ============================================================
|
||||||
|
|
||||||
if (obj.Operacao?.Pausa == true)
|
if (obj.Operacao?.Pausa == true)
|
||||||
{
|
{
|
||||||
|
var motivo =
|
||||||
|
NormalizarTexto(obj.Operacao.MotivoStatusOperacao)
|
||||||
|
?? "Pausa operacional solicitada.";
|
||||||
|
|
||||||
eventos.Add(new MotivoTopBarModel
|
eventos.Add(new MotivoTopBarModel
|
||||||
{
|
{
|
||||||
Prioridade = 900,
|
Prioridade = 900,
|
||||||
Fonte = "Pausa",
|
Fonte = "Pausa",
|
||||||
Titulo = "OPERAÇÃO PAUSADA",
|
Titulo = "OPERAÇÃO PAUSADA",
|
||||||
Descricao = "Pausa operacional solicitada.",
|
Descricao = motivo,
|
||||||
StatusVisual = StatusModulo.Alerta
|
StatusVisual = StatusModulo.Alerta
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================================================
|
||||||
// 3) Controle
|
// 4) Operação não liberada
|
||||||
// ============================
|
//
|
||||||
var motivosControle = obj.Controle?.Motivos?
|
// 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))
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList() ?? new List<string>();
|
.ToList()
|
||||||
|
?? new List<string>();
|
||||||
|
|
||||||
foreach (var motivo in motivosControle)
|
foreach (var motivo in motivosControle)
|
||||||
{
|
{
|
||||||
|
|
@ -628,13 +746,18 @@ namespace OperationControl.ViewModels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================================================
|
||||||
// 4) Trajetória / autonomia
|
// 6) Trajetória / autonomia
|
||||||
// ============================
|
// ============================================================
|
||||||
var motivosTrajetoria = obj.Trajetoria?.AutonomiaCorredor?.Motivos?
|
|
||||||
|
var motivosTrajetoria =
|
||||||
|
obj.Trajetoria?.AutonomiaCorredor?.Motivos?
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Select(NormalizarTexto)
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList() ?? new List<string>();
|
.ToList()
|
||||||
|
?? new List<string>();
|
||||||
|
|
||||||
foreach (var motivo in motivosTrajetoria)
|
foreach (var motivo in motivosTrajetoria)
|
||||||
{
|
{
|
||||||
|
|
@ -648,110 +771,215 @@ namespace OperationControl.ViewModels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================================================
|
||||||
// 5) Módulos com condições operacionais críticas
|
// 7) Estado operacional
|
||||||
// ============================
|
//
|
||||||
var modulosCriticos = obj.ModulosSaude?
|
// 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 =>
|
.Where(mod =>
|
||||||
(mod?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false) ||
|
{
|
||||||
(
|
if (mod == null)
|
||||||
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(mod?.status ?? StatusModulo.Desconectado) &&
|
return false;
|
||||||
(rover?.ModulosMandatorios?.Any(x =>
|
|
||||||
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) &&
|
var status =
|
||||||
(x?.Utilizar ?? false) &&
|
mod?.status ?? StatusModulo.Desconectado;
|
||||||
(x?.Mandatorio ?? false)
|
|
||||||
) ?? false)
|
bool temCondicaoSevera =
|
||||||
) ||
|
mod?.condicoes_operacionais?.Any(
|
||||||
(
|
cond => cond?.severidade > 90
|
||||||
(mod?.status ?? StatusModulo.Desconectado) == StatusModulo.Alerta &&
|
) ?? false;
|
||||||
(rover?.ModulosMandatorios?.Any(x =>
|
|
||||||
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) &&
|
bool mandatorio =
|
||||||
(x?.Utilizar ?? false) &&
|
EhModuloMandatorio(mod?.modulo ?? T_Code.Vzo);
|
||||||
(x?.Mandatorio ?? false)
|
|
||||||
) ?? false)
|
bool naoOperante =
|
||||||
)
|
!new[]
|
||||||
)
|
{
|
||||||
.ToList() ?? new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>();
|
StatusModulo.Operante,
|
||||||
|
StatusModulo.Alerta
|
||||||
|
}.Contains(status);
|
||||||
|
|
||||||
|
bool emAlerta =
|
||||||
|
status == StatusModulo.Alerta;
|
||||||
|
|
||||||
|
return
|
||||||
|
temCondicaoSevera ||
|
||||||
|
(mandatorio && naoOperante) ||
|
||||||
|
(mandatorio && emAlerta);
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
?? new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>();
|
||||||
|
|
||||||
foreach (var modulo in modulosCriticos)
|
foreach (var modulo in modulosCriticos)
|
||||||
{
|
{
|
||||||
var nomeModulo = (modulo?.modulo ?? T_Code.Vzo).ToString();
|
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;
|
||||||
|
|
||||||
bool temCondicaoSevera = modulo?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false;
|
|
||||||
bool moduloMandatorioNaoOperante =
|
bool moduloMandatorioNaoOperante =
|
||||||
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(modulo?.status ?? StatusModulo.Desconectado) &&
|
mandatorio &&
|
||||||
(rover?.ModulosMandatorios?.Any(x =>
|
status != StatusModulo.Operante &&
|
||||||
(x?.Dispositivo ?? T_Code.Vzo) == (modulo?.modulo ?? T_Code.Vzo) &&
|
status != StatusModulo.Alerta;
|
||||||
(x?.Utilizar ?? false) &&
|
|
||||||
(x?.Mandatorio ?? false)
|
bool moduloMandatorioAlerta =
|
||||||
) ?? false);
|
mandatorio &&
|
||||||
bool moduloMandatorioAlerta =
|
status == StatusModulo.Alerta;
|
||||||
(modulo?.status ?? StatusModulo.Desconectado) == StatusModulo.Alerta &&
|
|
||||||
(rover?.ModulosMandatorios?.Any(x =>
|
// --------------------------------------------------------
|
||||||
(x?.Dispositivo ?? T_Code.Vzo) == (modulo?.modulo ?? T_Code.Vzo) &&
|
// 8.1) Mandatório não operacional
|
||||||
(x?.Utilizar ?? false) &&
|
// --------------------------------------------------------
|
||||||
(x?.Mandatorio ?? false)
|
|
||||||
) ?? false);
|
|
||||||
|
|
||||||
// Caso 1: módulo mandatório não operante
|
|
||||||
if (moduloMandatorioNaoOperante)
|
if (moduloMandatorioNaoOperante)
|
||||||
{
|
{
|
||||||
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
|
List<string> motivos =
|
||||||
string motivos = string.Join("; ", modulo?.motivos ?? new List<string>() { "Desconhecido" });
|
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.";
|
||||||
|
|
||||||
eventos.Add(new MotivoTopBarModel
|
eventos.Add(new MotivoTopBarModel
|
||||||
{
|
{
|
||||||
Prioridade = 620,
|
Prioridade = 620,
|
||||||
Fonte = "ModuloCritico",
|
Fonte = "ModuloCritico",
|
||||||
Titulo = "MÓDULO MANDATÓRIO NÃO OPERACIONAL",
|
Titulo = "MÓDULO MANDATÓRIO NÃO OPERACIONAL",
|
||||||
Descricao = $"{nomeModulo}: módulo mandatório em estado '{statusTexto}': {motivos}",
|
Descricao =
|
||||||
|
$"{nomeModulo}: módulo mandatório em estado " +
|
||||||
|
$"'{status}': {motivoTexto}",
|
||||||
StatusVisual = StatusModulo.Falha,
|
StatusVisual = StatusModulo.Falha,
|
||||||
Modulo = modulo?.modulo
|
Modulo = codigoModulo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso 2: condições operacionais severas
|
// --------------------------------------------------------
|
||||||
|
// 8.2) Condições operacionais severas
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
if (temCondicaoSevera)
|
if (temCondicaoSevera)
|
||||||
{
|
{
|
||||||
var descricoesCriticas = modulo?.condicoes_operacionais?
|
var descricoesCriticas =
|
||||||
|
modulo?.condicoes_operacionais?
|
||||||
.Where(cond => cond?.severidade > 90)
|
.Where(cond => cond?.severidade > 90)
|
||||||
.Select(cond => cond?.descricao ?? "")
|
.Select(cond =>
|
||||||
.Where(desc => !string.IsNullOrWhiteSpace(desc))
|
NormalizarTexto(cond?.descricao))
|
||||||
|
.Where(desc =>
|
||||||
|
!string.IsNullOrWhiteSpace(desc))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList() ?? new List<string>();
|
.ToList()
|
||||||
|
?? new List<string>();
|
||||||
|
|
||||||
foreach (var desc in descricoesCriticas)
|
foreach (var descricao in descricoesCriticas)
|
||||||
{
|
{
|
||||||
eventos.Add(new MotivoTopBarModel
|
eventos.Add(new MotivoTopBarModel
|
||||||
{
|
{
|
||||||
Prioridade = 610,
|
Prioridade = 610,
|
||||||
Fonte = "ModuloCritico",
|
Fonte = "ModuloCritico",
|
||||||
Titulo = "CONDIÇÕES OPERACIONAIS CRÍTICAS",
|
Titulo = "CONDIÇÕES OPERACIONAIS CRÍTICAS",
|
||||||
Descricao = $"{nomeModulo}: {desc}",
|
Descricao =
|
||||||
|
$"{nomeModulo}: {descricao}",
|
||||||
StatusVisual = StatusModulo.Alerta,
|
StatusVisual = StatusModulo.Alerta,
|
||||||
Modulo = modulo?.modulo
|
Modulo = codigoModulo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso 3: módulo mandatório em alerta
|
// --------------------------------------------------------
|
||||||
|
// 8.3) Mandatório em alerta
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
if (moduloMandatorioAlerta)
|
if (moduloMandatorioAlerta)
|
||||||
{
|
{
|
||||||
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
|
List<string> motivos =
|
||||||
string motivos = string.Join("; ", modulo?.motivos ?? new List<string>() { "Desconhecido" });
|
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.";
|
||||||
|
|
||||||
eventos.Add(new MotivoTopBarModel
|
eventos.Add(new MotivoTopBarModel
|
||||||
{
|
{
|
||||||
Prioridade = 600,
|
Prioridade = 600,
|
||||||
Fonte = "ModuloCritico",
|
Fonte = "ModuloCritico",
|
||||||
Titulo = "MÓDULO MANDATÓRIO EM ALERTA",
|
Titulo = "MÓDULO MANDATÓRIO EM ALERTA",
|
||||||
Descricao = $"{nomeModulo}: módulo mandatório em estado '{statusTexto}': {motivos}",
|
Descricao =
|
||||||
|
$"{nomeModulo}: módulo mandatório em estado " +
|
||||||
|
$"'{status}': {motivoTexto}",
|
||||||
StatusVisual = StatusModulo.Alerta,
|
StatusVisual = StatusModulo.Alerta,
|
||||||
Modulo = modulo?.modulo
|
Modulo = codigoModulo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 9) Ordenação dos módulos
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
var ordemModulos = new Dictionary<T_Code, int>
|
var ordemModulos = new Dictionary<T_Code, int>
|
||||||
{
|
{
|
||||||
{ T_Code.Ipb, 1 },
|
{ T_Code.Ipb, 1 },
|
||||||
|
|
@ -763,18 +991,39 @@ namespace OperationControl.ViewModels
|
||||||
{ T_Code.Sen, 7 }
|
{ T_Code.Sen, 7 }
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================
|
// ============================================================
|
||||||
// 6) Remove vazios / repetidos
|
// 10) Limpeza / ordenação dos eventos
|
||||||
// ============================
|
// ============================================================
|
||||||
var eventosOrdenados = eventos
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.Titulo) && !string.IsNullOrWhiteSpace(x.Descricao))
|
var eventosOrdenados =
|
||||||
.GroupBy(x => new { x.Fonte, x.Titulo, x.Descricao, x.Prioridade, x.StatusVisual })
|
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())
|
.Select(g => g.First())
|
||||||
.OrderByDescending(x => x.Prioridade)
|
.OrderByDescending(x => x.Prioridade)
|
||||||
.ThenBy(x => x.Modulo.HasValue && ordemModulos.ContainsKey(x.Modulo.Value)? ordemModulos[x.Modulo.Value] : int.MaxValue)
|
.ThenBy(x =>
|
||||||
|
x.Modulo.HasValue &&
|
||||||
|
ordemModulos.TryGetValue(
|
||||||
|
x.Modulo.Value,
|
||||||
|
out var ordem)
|
||||||
|
? ordem
|
||||||
|
: int.MaxValue)
|
||||||
.ThenBy(x => x.Fonte)
|
.ThenBy(x => x.Fonte)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 11) Estado normal
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
if (!eventosOrdenados.Any())
|
if (!eventosOrdenados.Any())
|
||||||
{
|
{
|
||||||
eventosOrdenados.Add(new MotivoTopBarModel
|
eventosOrdenados.Add(new MotivoTopBarModel
|
||||||
|
|
@ -782,96 +1031,163 @@ namespace OperationControl.ViewModels
|
||||||
Prioridade = 0,
|
Prioridade = 0,
|
||||||
Fonte = "Normal",
|
Fonte = "Normal",
|
||||||
Titulo = "ROVER OPERANDO NORMALMENTE",
|
Titulo = "ROVER OPERANDO NORMALMENTE",
|
||||||
Descricao = "Trajetória, controle e operação em condição normal.",
|
Descricao =
|
||||||
StatusVisual = obj.StatusRover == StatusModulo.Desconectado
|
"Trajetória, controle e operação em condição normal.",
|
||||||
|
StatusVisual =
|
||||||
|
obj.StatusRover == StatusModulo.Desconectado
|
||||||
? StatusModulo.Desconectado
|
? StatusModulo.Desconectado
|
||||||
: StatusModulo.Operante
|
: StatusModulo.Operante
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================================================
|
||||||
// 7) Monta visual final
|
// 12) Montagem visual
|
||||||
// ============================
|
// ============================================================
|
||||||
StatusModulo statusVisual;
|
|
||||||
string linha1;
|
var principal =
|
||||||
|
eventosOrdenados.First();
|
||||||
|
|
||||||
|
StatusModulo statusVisual =
|
||||||
|
principal.StatusVisual;
|
||||||
|
|
||||||
|
string linha1 =
|
||||||
|
principal.Titulo;
|
||||||
|
|
||||||
string linha2;
|
string linha2;
|
||||||
string badge;
|
|
||||||
string resumo;
|
|
||||||
|
|
||||||
if (eventosOrdenados.Any())
|
// 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")
|
||||||
{
|
{
|
||||||
var principal = eventosOrdenados.First();
|
linha2 = principal.Descricao;
|
||||||
|
}
|
||||||
statusVisual = principal.StatusVisual;
|
else
|
||||||
linha1 = principal.Titulo;
|
{
|
||||||
|
var descricoes =
|
||||||
// Pega até 3 descrições distintas, por prioridade
|
eventosOrdenados
|
||||||
var descricoes = eventosOrdenados
|
|
||||||
.Select(x => x.Descricao?.Trim())
|
.Select(x => x.Descricao?.Trim())
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Take(3)
|
.Take(3)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
linha2 = string.Join(" • ", descricoes);
|
linha2 =
|
||||||
|
string.Join(" • ", descricoes);
|
||||||
|
}
|
||||||
|
|
||||||
// Badge curto
|
string badge =
|
||||||
badge = principal.Fonte switch
|
principal.Fonte switch
|
||||||
{
|
{
|
||||||
"Comunicacao" => "DESCONECTADO",
|
"Comunicacao" => "DESCONECTADO",
|
||||||
|
"NCP" => "DESCONECTADO",
|
||||||
"Emergencia" => "EMERGÊNCIA",
|
"Emergencia" => "EMERGÊNCIA",
|
||||||
"Pausa" => "PAUSADO",
|
"Pausa" => "PAUSADO",
|
||||||
"Trajetoria" => "TRAJETÓRIA",
|
"Operacao" => "BLOQUEADO",
|
||||||
"Controle" => "CONTROLE",
|
"Controle" => "CONTROLE",
|
||||||
|
"Trajetoria" => "TRAJETÓRIA",
|
||||||
|
"StatusOperacao"
|
||||||
|
=> obj.Operacao?.Status
|
||||||
|
.ToString()
|
||||||
|
.ToUpper() ?? "OPERAÇÃO",
|
||||||
"ModuloCritico" => "ALERTA",
|
"ModuloCritico" => "ALERTA",
|
||||||
_ => statusVisual.ToString().ToUpper()
|
"Normal" => statusVisual
|
||||||
|
.ToString()
|
||||||
|
.ToUpper(),
|
||||||
|
_ => statusVisual
|
||||||
|
.ToString()
|
||||||
|
.ToUpper()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resumo curto
|
string resumo =
|
||||||
resumo = principal.Fonte switch
|
principal.Fonte switch
|
||||||
{
|
{
|
||||||
"Comunicacao" => "Sem telemetria",
|
"Comunicacao" =>
|
||||||
"Emergencia" => "Parada imediata",
|
"Sem telemetria",
|
||||||
"Pausa" => "Aguardando retomada",
|
|
||||||
"Trajetoria" => "Operação bloqueada",
|
"NCP" =>
|
||||||
"Controle" => "Controle bloqueado",
|
"Núcleo central desconectado",
|
||||||
"ModuloCritico" => "Atenção operacional",
|
|
||||||
_ => (obj.Operacao?.Status ?? StatusOperacao.NaoIniciado).ToString()
|
"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()
|
||||||
};
|
};
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statusVisual = obj.StatusRover == StatusModulo.Desconectado
|
|
||||||
? StatusModulo.Desconectado
|
|
||||||
: StatusModulo.Operante;
|
|
||||||
|
|
||||||
linha1 = "ROVER OPERANDO NORMALMENTE";
|
// ============================================================
|
||||||
linha2 = "Trajetória, controle e operação em condição normal.";
|
// 13) Histórico
|
||||||
badge = statusVisual.ToString().ToUpper();
|
// ============================================================
|
||||||
resumo = (obj.Operacao?.Status ?? StatusOperacao.NaoIniciado).ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
_viewOperacaoCenter?.Historico?._vm?.CompararERegistrarMudancasEventos(rover.RoverId, eventosOrdenados);
|
_viewOperacaoCenter?
|
||||||
|
.Historico?
|
||||||
|
._vm?
|
||||||
|
.CompararERegistrarMudancasEventos(
|
||||||
|
rover.RoverId,
|
||||||
|
eventosOrdenados);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 14) Segurança visual
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(linha1))
|
||||||
|
linha1 = "ESTADO DO ROVER";
|
||||||
|
|
||||||
// Segurança extra: evita linha vazia
|
|
||||||
if (string.IsNullOrWhiteSpace(linha2))
|
if (string.IsNullOrWhiteSpace(linha2))
|
||||||
linha2 = "Sem detalhes adicionais.";
|
linha2 = "Sem detalhes adicionais.";
|
||||||
|
|
||||||
// ============================
|
if (string.IsNullOrWhiteSpace(badge))
|
||||||
// 8) Atualiza UI
|
badge = "STATUS";
|
||||||
// ============================
|
|
||||||
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
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(
|
_viewOperacaoTop?._vm?.AtualizarStatus(
|
||||||
statusVisual,
|
statusVisual,
|
||||||
rover.RoverId,
|
rover.RoverId,
|
||||||
obj?.Operacao?.Status ?? StatusOperacao.NaoIniciado,
|
obj.Operacao?.Status
|
||||||
|
?? StatusOperacao.NaoIniciado,
|
||||||
linha1,
|
linha1,
|
||||||
linha2,
|
linha2,
|
||||||
badge,
|
badge,
|
||||||
resumo
|
resumo
|
||||||
);
|
);
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AtualizarBarraSuperior(StatusModulo status, string titulo, string linha1, string linha2, string status_str, string resumo)
|
public void AtualizarBarraSuperior(StatusModulo status, string titulo, string linha1, string linha2, string status_str, string resumo)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue