Compare commits

..

No commits in common. "b737b11a592ed0de5867d3a61388ba235e0d3eef" and "00c9af952a2ae81b253e620ce892138fd6048271" have entirely different histories.

18 changed files with 488 additions and 4618 deletions

View File

@ -1,4 +1,4 @@
using AgroBase.Models;
using AgroBase.Models;
using System;
using System.Windows.Forms;
@ -52,8 +52,6 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
private void CarregarDadosTela()
{
_carregandoDados = true;
try
{
var parametros = op?.Parametros?.Controle;
@ -99,12 +97,9 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
parametros.AtuAgitadorModo = seletorModoAgitador.SelectedMode;
if (agitador != null)
{
agitador.Kp = (double)paramKp.Value;
agitador.Ki = (double)paramKi.Value;
agitador.Kd = (double)paramKd.Value;
}
agitador.Kp = (double)paramKp.Value;
agitador.Ki = (double)paramKi.Value;
agitador.Kd = (double)paramKd.Value;
parametros.AtuDuracaoAtuacao = (int)paramDuracaoAtuacao.Value;
parametros.AtuPressaoLinha = (double)paramPressaoLinha.Value;

View File

@ -1,4 +1,4 @@
using AgroBase.Forms.IHM.Controls;
using AgroBase.Forms.IHM.Controls;
using AgroBase.Models;
using System;
using System.Collections.Generic;
@ -14,7 +14,6 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
private bool _carregandoDados;
private bool _mapaExistenteCarregado;
private bool _hidratandoMapaExistente;
public ucParametrosMapa()
{
@ -57,12 +56,8 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
if (mesmaInstanciaMapa && !recarregarMapa)
{
_mapaExistenteCarregado = true;
lblMapaPlaceholder.Visible =
(_mapa.mapaService?.DadosMapa ?? op.Parametros?.Mapa) == null;
// Ao apenas reanexar a mesma instância, nunca sobrescrevemos
// Parametros.RuasPercorrer com uma lista runtime vazia.
SincronizarParametrosComMapa(permitirSelecaoVazia: false);
lblMapaPlaceholder.Visible = _mapa.mapaService?.DadosMapa == null;
SincronizarParametrosComMapa();
AtualizarResumo();
return;
}
@ -77,28 +72,17 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
try
{
MapaFeatureCollectionModel mapaRuntime =
_mapa?.mapaService?.DadosMapa;
lblMapaPlaceholder.Visible = _mapa?.mapaService?.DadosMapa == null;
lblMapaPlaceholder.Visible =
(mapaRuntime ?? op?.Parametros?.Mapa) == null;
SincronizarParametrosComMapa();
AtualizarResumo();
/*
* Primeiro preservamos o snapshot da operação. Se o MapasModel
* runtime ainda não foi hidratado, copiar dele agora apagaria
* Mapa/RuasPercorrer dos parâmetros.
*/
if (!_mapaExistenteCarregado && op?.Parametros?.Mapa != null)
{
_mapaExistenteCarregado = true;
AtualizarResumo();
CarregarMapaExistenteAsync();
}
else
{
SincronizarParametrosComMapa(permitirSelecaoVazia: false);
AtualizarResumo();
}
}
finally
{
@ -108,57 +92,28 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
private async void CarregarMapaExistenteAsync()
{
if (_hidratandoMapaExistente)
return;
_hidratandoMapaExistente = true;
try
{
if (_mapa == null || op?.Parametros?.Mapa == null)
{
return;
}
/*
* Snapshot antes do primeiro await. Eventos do mapa podem ocorrer
* durante CarregarMapaAsync; eles não podem apagar esta seleção.
*/
List<string> ruasSelecionadas =
op.Parametros.RuasPercorrer != null
? new List<string>(op.Parametros.RuasPercorrer)
: new List<string>();
List<string> ruasSelecionadas = op.Parametros.RuasPercorrer != null ? new List<string>(op.Parametros.RuasPercorrer) : new List<string>();
await _mapa.CarregarMapaAsync(
op.Parametros.Mapa,
"Mapa da operação"
);
await _mapa.CarregarMapaAsync(op.Parametros.Mapa, "Mapa da operação");
_mapa.TipoMapa = op.Parametros.TipoMapa;
await _mapa.DefinirRuasSelecionadasAsync(
op,
ruasSelecionadas,
false
);
/*
* Agora o runtime está hidratado. Sincronizamos uma única vez,
* inclusive aceitando lista vazia quando ela realmente veio dos
* parâmetros da operação.
*/
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
await _mapa.DefinirRuasSelecionadasAsync(op, ruasSelecionadas, false);
lblMapaPlaceholder.Visible = false;
AtualizarResumo();
}
catch (Exception ex)
{
Variaveis.MostrarLog(
"[ucParametrosMapa.CarregarMapaExistente] " + ex.Message
);
}
finally
{
_hidratandoMapaExistente = false;
Variaveis.MostrarLog("[ucParametrosMapa.CarregarMapaExistente] " + ex.Message);
}
}
@ -181,9 +136,6 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
if (IsDisposed || Disposing)
return;
if (_hidratandoMapaExistente)
return;
if (InvokeRequired)
{
BeginInvoke(new Action(
@ -198,13 +150,13 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
lblMapaPlaceholder.Visible = false;
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
SincronizarParametrosComMapa();
AtualizarResumo();
}
private void Mapa_RuasSelecionadasAlteradas(object sender, EventArgs e)
{
if (_carregandoDados || _hidratandoMapaExistente)
if (_carregandoDados)
return;
if (IsDisposed || Disposing)
@ -222,52 +174,27 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
return;
}
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
SincronizarParametrosComMapa();
AtualizarResumo();
}
private void SincronizarParametrosComMapa(bool permitirSelecaoVazia)
private void SincronizarParametrosComMapa()
{
if (op?.Parametros == null || _mapa == null)
{
return;
}
MapaFeatureCollectionModel dadosMapa =
_mapa.mapaService?.DadosMapa;
/*
* A tela de parâmetros não é a fonte primária do mapa durante uma
* operação em andamento. Se o WebView/MapasModel ainda não carregou,
* mantemos intacto o snapshot que já veio em op.Parametros.
*/
if (dadosMapa == null)
return;
op.Parametros.Mapa = dadosMapa;
op.Parametros.Mapa = _mapa.mapaService?.DadosMapa;
op.Parametros.TipoMapa = _mapa.TipoMapa;
List<string> ruasRuntime =
_mapa.RuasPercorrer != null
? _mapa.RuasPercorrer.ToList()
: new List<string>();
if (permitirSelecaoVazia || ruasRuntime.Count > 0)
op.Parametros.RuasPercorrer = ruasRuntime;
op.Parametros.RuasPercorrer = _mapa.RuasPercorrer != null ? _mapa.RuasPercorrer.ToList() : new List<string>();
}
private void AtualizarResumo()
{
MapaFeatureCollectionModel dadosMapa =
_mapa?.mapaService?.DadosMapa ??
op?.Parametros?.Mapa;
List<MapaFeatureModel> features =
dadosMapa?.features ??
new List<MapaFeatureModel>();
List<string> selecionadas =
(_mapa?.RuasPercorrer?.Count ?? 0) > 0
? _mapa.RuasPercorrer
: (op?.Parametros?.RuasPercorrer ?? new List<string>());
MapaFeatureCollectionModel dadosMapa = _mapa?.mapaService?.DadosMapa;
List<MapaFeatureModel> features = dadosMapa?.features ?? new List<MapaFeatureModel>();
List<string> selecionadas = _mapa?.RuasPercorrer ?? new List<string>();
int quantidadeTotal = features.Count;
int quantidadeSelecionada = selecionadas.Count;
double distanciaTotalMapa = features

View File

@ -1,4 +1,4 @@
using AgroBase.Forms.IHM.Controls;
using AgroBase.Forms.IHM.Controls;
using AgroBase.Models;
using System;
using System.Collections.Generic;
@ -193,8 +193,6 @@ namespace AgroBase.Forms.IHM.Operacao
pnlAlertaAutonomia.Visible = false;
pnlStatusAutonomia.Visible = true;
btnLiberarSupervisao.Enabled = false;
pnlStatusAutonomia.BringToFront();
}
@ -206,36 +204,6 @@ namespace AgroBase.Forms.IHM.Operacao
pnlAlertaAutonomia.BringToFront();
}
private static bool PodeLiberarSupervisao(AutonomiaCorredorModel autonomia)
{
if (autonomia == null || autonomia.Liberado)
return false;
bool bateriaBloqueando =
autonomia.DadosBateriaValidos &&
!autonomia.BateriaSuficiente &&
!autonomia.BateriaOverrideAplicado;
bool herbicidaBloqueando =
autonomia.PulverizacaoExigida &&
autonomia.DadosHerbicidaValidos &&
!autonomia.HerbicidaSuficiente &&
!autonomia.HerbicidaOverrideAplicado;
/*
* AguardandoLiberacaoManual representa uma trava persistente que
* precisa de reconhecimento humano mesmo se o recurso já recuperou.
* Critico representa uma insuficiência atual e também deve permitir
* a decisão supervisionada, sem obrigar o operador a esperar o latch.
*/
return
autonomia.Status == AutonomiaCorredorStatus.AguardandoLiberacaoManual ||
(
autonomia.Status == AutonomiaCorredorStatus.Critico &&
(bateriaBloqueando || herbicidaBloqueando)
);
}
private int ObterIndiceCorredorAvaliado()
{
int indiceAtual =
@ -259,44 +227,8 @@ namespace AgroBase.Forms.IHM.Operacao
private void btnLiberarSupervisao_Click(object sender, EventArgs e)
{
var autonomia = op?.Trajetoria?.AutonomiaCorredor;
if (!PodeLiberarSupervisao(autonomia))
return;
int idxCorredor = ObterIndiceCorredorAvaliado();
bool bateriaBloqueando =
autonomia.DadosBateriaValidos &&
!autonomia.BateriaSuficiente &&
!autonomia.BateriaOverrideAplicado;
bool herbicidaBloqueando =
autonomia.PulverizacaoExigida &&
autonomia.DadosHerbicidaValidos &&
!autonomia.HerbicidaSuficiente &&
!autonomia.HerbicidaOverrideAplicado;
bool aguardandoReconhecimento =
autonomia.Status == AutonomiaCorredorStatus.AguardandoLiberacaoManual;
/*
* Em AguardandoLiberacaoManual o motivo original da trava pode já
* ter se recuperado, e o modelo não expõe publicamente qual latch
* ficou pendente. Nesse caso reconhecemos ambos, como a implementação
* antiga já fazia.
*
* Em Critico liberamos apenas o recurso que realmente está bloqueando.
*/
bool liberarBateria = aguardandoReconhecimento || bateriaBloqueando;
bool liberarHerbicida = aguardandoReconhecimento || herbicidaBloqueando;
op?.Trajetoria?.AtualizarDadosAutonomiaCorredor(
idxCorredor: idxCorredor,
bat_liberada: liberarBateria,
herb_liberado: liberarHerbicida
);
op?.Trajetoria?.AtualizarDadosAutonomiaCorredor(idxCorredor: idxCorredor, bat_liberada: true, herb_liberado: true);
AtualizarResultadoAutonomia(op?.Trajetoria?.AutonomiaCorredor);
}
@ -535,21 +467,13 @@ namespace AgroBase.Forms.IHM.Operacao
bool operacaoIniciada = op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false;
bool podeLiberarSupervisao = PodeLiberarSupervisao(autonomia);
bool aguardandoLiberacao = autonomia.Status == AutonomiaCorredorStatus.AguardandoLiberacaoManual && !autonomia.Liberado;
btnLiberarSupervisao.Enabled =
operacaoIniciada &&
podeLiberarSupervisao;
if (operacaoIniciada && podeLiberarSupervisao)
if (operacaoIniciada && aguardandoLiberacao)
{
AtualizarTextoAlertaAutonomia(autonomia);
MostrarAlertaAutonomia();
// MostrarStatusAutonomia() desabilita o botão, então restauramos
// explicitamente o estado após trazer o painel de alerta.
btnLiberarSupervisao.Enabled = true;
return;
}
@ -559,13 +483,11 @@ namespace AgroBase.Forms.IHM.Operacao
private void AtualizarTextoAlertaAutonomia(AutonomiaCorredorModel autonomia)
{
bool bateriaBloqueando =
autonomia.DadosBateriaValidos &&
!autonomia.BateriaSuficiente &&
!autonomia.BateriaOverrideAplicado;
bool herbicidaBloqueando =
autonomia.PulverizacaoExigida &&
autonomia.DadosHerbicidaValidos &&
!autonomia.HerbicidaSuficiente &&
!autonomia.HerbicidaOverrideAplicado;
@ -722,7 +644,10 @@ namespace AgroBase.Forms.IHM.Operacao
lblStatusAvaliacao.Text = !string.IsNullOrWhiteSpace(autonomia.Motivo) ? autonomia.Motivo : "Autonomia atualizada";
lblStatusAvaliacao.ForeColor = autonomia.Liberado ? corCorredor : HmiTheme.TextMuted;
AtualizarPainelAutonomia(autonomia);
if (autonomia.Liberado)
MostrarStatusAutonomia();
else
MostrarAlertaAutonomia();
}

View File

@ -1,4 +1,4 @@
using AgroBase.Forms.IHM.Controls;
using AgroBase.Forms.IHM.Controls;
using AgroBase.Forms.IHM.Operacao.Parametros;
using AgroBase.Models;
using System;
@ -155,36 +155,18 @@ namespace AgroBase.Forms.IHM.Operacao
if (operacaoParametrizando == null)
return;
var op = Variaveis.OperacaoEmAndamento;
bool operacaoIniciada =
op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false;
/*
* A exigência de mapa/ruas é válida para parametrizar uma operação
* nova. Durante uma operação já iniciada, o botão Salvar realiza
* atualização parcial (pulverizador, agitador, velocidades etc.) e
* não deve ser bloqueado por uma validação estrutural do mapa.
*/
if (!operacaoIniciada &&
operacaoParametrizando.Parametros.Modo == ModoOperacao.MapaGPS)
if (operacaoParametrizando.Parametros.Modo == ModoOperacao.MapaGPS && (operacaoParametrizando.Mapa.RuasPercorrer?.Count ?? 0) == 0)
{
int qtdRuas =
operacaoParametrizando.Parametros?.RuasPercorrer?.Count
?? operacaoParametrizando.Mapa?.RuasPercorrer?.Count
?? 0;
if (qtdRuas == 0)
{
FuncoesIHM.MostrarAlerta(
"Trajetória não parametrizada",
$"Para salvar os parâmetros da operação {operacaoParametrizando.Sensoriamento.Operacao.Modo}, é necessário configurar o mapa e as ruas a serem percorridas!",
MessageBoxIcon.Warning
);
return;
}
FuncoesIHM.MostrarAlerta(
"Trajetória não parametrizada",
$"Para salvar os parâmetros da operação {operacaoParametrizando.Sensoriamento.Operacao.Modo}, é necessário configurar o mapa e as ruas a serem percorridas!",
MessageBoxIcon.Warning
);
return;
}
if (operacaoIniciada)
var op = Variaveis.OperacaoEmAndamento;
if (op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false)
op?.AtualizarParametrosParciais(operacaoParametrizando.Parametros, op);
else
OperacaoModel.CarregarParamerosOperacaoBase(operacaoParametrizando.Parametros);

View File

@ -772,7 +772,7 @@ namespace AgroBase.Forms
lblDistProx.Text =
$"Distância Próximo Ponto: {(proximoPonto?.DistanciaAtual ?? 0):F2} m";
lblDistAnt.Text =
$"Esq: {dadosTrajetoria.DistanciaEsquerda * 100:F0} cm | Dir: {dadosTrajetoria.DistanciaDireita * 100:F0} cm | Err: {(dadosTrajetoria.DistanciaDireita - dadosTrajetoria.DistanciaEsquerda) * 100:F0} cm";
$"Dist Esquerda: {dadosTrajetoria.DistanciaEsquerda * 100:F0} cm | Dist Direita: {dadosTrajetoria.DistanciaDireita * 100:F0} cm";
lblTempoRestante.Text =
$"Tempo Restante: {dadosTrajetoria.TempoEstimadoRestante}";
lblTempoOperacao.Text =
@ -1446,15 +1446,6 @@ namespace AgroBase.Forms
Interlocked.Exchange(ref _forcarTrajetoriaWeb, 1);
AtualizarDadosTela();
for (int idxCorredor = 0; idxCorredor < Variaveis.OperacaoEmAndamento.Trajetoria.Corredores.Count; idxCorredor++)
{
Variaveis.OperacaoEmAndamento.Trajetoria.AtualizarDadosAutonomiaCorredor(
idxCorredor: idxCorredor,
bat_liberada: true,
herb_liberado: true
);
}
}
}
catch (Exception ex)

View File

@ -2599,24 +2599,12 @@ namespace AgroBase.Models.Modules
if (op?.DispAtu?.Dados == null)
return tasks;
// Mantido por compatibilidade com os chamadores antigos.
// Task<bool> deriva de Task, então o contrato List<Task> continua válido.
tasks.Add(RealizarCalibragemComResultadoAsync(forcar));
tasks.Add(ExecutarCalibragemProtegidaAsync(forcar));
return tasks;
}
public Task<bool> RealizarCalibragemComResultadoAsync(bool forcar)
{
var op = Variaveis.OperacaoEmAndamento;
if (op?.DispAtu?.Dados == null)
return Task.FromResult(false);
return ExecutarCalibragemProtegidaAsync(forcar);
}
private async Task<bool> ExecutarCalibragemProtegidaAsync(bool forcar)
private async Task ExecutarCalibragemProtegidaAsync(bool forcar)
{
var op = Variaveis.OperacaoEmAndamento;
@ -2649,15 +2637,11 @@ namespace AgroBase.Models.Modules
op.Sensoriamento.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Teste do módulo {Modulo_ID} finalizado com pendências: " + string.Join(", ", v));
}
return sucesso;
}
catch (OperationCanceledException ex)
{
Variaveis.MostrarLog($"[AtuadorModel] [RealizarCalibragemAsync] Calibragem cancelada: {ex.Message}");
op?.Sensoriamento?.InserirLog(Dispositivo, StatusModulo.Alerta, 50, $"Calibragem do módulo {Modulo_ID} cancelada: {ex.Message}");
return false;
}
catch (Exception ex)
{

View File

@ -1,4 +1,4 @@
using AgroBase.Comum;
using AgroBase.Comum;
using AgroBase.Forms;
using AgroBase.Forms.Operacoes;
using AgroBase.Models.Modules;
@ -349,12 +349,6 @@ namespace AgroBase.Models
private static string _discoverySessionId = Guid.NewGuid().ToString("N");
private static long _telemetrySequence = 0;
private readonly SemaphoreSlim _calibragemGate = new SemaphoreSlim(1, 1);
// A calibração inicial é uma etapa transacional da partida da operação.
// Depois que ela efetivamente entrou em calibração e terminou, não permitimos
// que o timer periódico pare o rover novamente para repetir a mesma rotina.
private volatile bool _calibragemInicialConcluida = false;
public bool CalibrandoRuntime
{
get
@ -367,12 +361,6 @@ namespace AgroBase.Models
private readonly TimeSpan _cooldownCalibragem = TimeSpan.FromSeconds(30);
private bool PodeTentarCalibragemAutomatica()
{
// Depois da calibração inicial efetivamente executada, qualquer pendência
// deve permanecer como diagnóstico/alerta. Não paramos o rover novamente
// no meio da operação para recalibrar automaticamente.
if (_calibragemInicialConcluida)
return false;
return DateTime.Now - _ultimaTentativaCalibragem > _cooldownCalibragem;
}
private void RegistrarTentativaCalibragem()
@ -1407,11 +1395,6 @@ namespace AgroBase.Models
op.Sensoriamento.UltimoRegistroLog = agora;
op.idxLog = 0;
// Cada nova operação ganha um ciclo de calibração novo.
op._calibragemInicialConcluida = false;
op._ultimaTentativaCalibragem = DateTime.MinValue;
op.IniciarTimerLogsOperacao();
RedisService.Publish(CmdKey.ManagerWorkerRx, JsonConvert.SerializeObject(new { cmd = ManagerWorkerCommandType.IniciarMPC }));
@ -1682,12 +1665,6 @@ namespace AgroBase.Models
if (!await _calibragemGate.WaitAsync(0))
return;
// A calibração forçada do início também conta como tentativa.
// Isso evita que o timer periódico considere o cooldown vencido logo
// após a partida da operação.
if (forcar)
op.RegistrarTentativaCalibragem();
TipoMovimentoDirecional tipoControle = op.Controle.TipoMovimento;
_pausaAntesCalibragem = op.Sensoriamento.Operacao.Pausa;
bool entrouEmCalibragem = false;
@ -1794,16 +1771,9 @@ namespace AgroBase.Models
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem dos módulos iniciada...");
var tasks = new List<Task>();
Task<bool> taskCalibragemAtu = null;
if (calibragemAtuNecessaria)
{
// Para o ATU consumimos o resultado real da calibração.
// Uma Task concluída não significa necessariamente que bomba/bicos
// passaram nos testes.
taskCalibragemAtu = dispAtu.Dados.RealizarCalibragemComResultadoAsync(forcar);
tasks.Add(taskCalibragemAtu);
}
tasks.AddRange(dispAtu.Dados.RealizarCalibragemAsync(forcar));
if (calibragemDirNecessaria)
tasks.AddRange(dispMvd.Dados.RealizarCalibragemDirAsync(forcar));
@ -1829,56 +1799,12 @@ namespace AgroBase.Models
{
await allTasks;
bool atuSucesso =
taskCalibragemAtu == null ||
(taskCalibragemAtu.Status == TaskStatus.RanToCompletion && taskCalibragemAtu.Result);
Variaveis.MostrarLog(
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
"Tasks de calibragem dos módulos concluídas."
);
var pendencias = op.Sensoriamento?.Operacao?.ModsCalibragem?
.SelectMany(modulo =>
(modulo.Value ?? new Dictionary<string, bool>())
.Where(item => !item.Value)
.Select(item => $"{modulo.Key}:{item.Key}")
)
.ToList()
?? new List<string>();
bool calibragemComPendencias = !atuSucesso || pendencias.Any();
if (calibragemComPendencias)
{
string detalhe = pendencias.Any()
? string.Join(", ", pendencias)
: "ATU reportou pendência sem item individual identificado";
string mensagem =
$"Calibragem inicial concluída com pendências: {detalhe}. " +
"A operação será liberada sem nova recalibração automática nesta execução.";
Variaveis.MostrarLog(
"[OperacaoModel.RealizarCalibragemInicialAsync] " + mensagem
);
op.Sensoriamento.InserirLog(
T_Code.Mod,
StatusModulo.Alerta,
50,
mensagem
);
}
else
{
Variaveis.MostrarLog(
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
"Calibragem dos módulos concluída com sucesso."
);
op.Sensoriamento.InserirLog(
T_Code.Mod,
StatusModulo.Operante,
100,
"Calibragem finalizada com sucesso."
);
}
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem finalizada.");
}
else
{
@ -1951,17 +1877,6 @@ namespace AgroBase.Models
);
}
if (forcar && entrouEmCalibragem)
{
op._calibragemInicialConcluida = true;
Variaveis.MostrarLog(
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
"Ciclo de calibração inicial encerrado. Recalibração automática " +
"durante esta operação ficará desabilitada."
);
}
try
{
_calibragemGate.Release();
@ -3376,9 +3291,10 @@ namespace AgroBase.Models
op.RegistrarTentativaCalibragem();
_ = Task.Run(async () => await op.RealizarCalibragemInicialAsync(false));
}
// O estado Calibrando pertence exclusivamente à rotina de calibração.
// O timer não deve liberar a operação antes das Tasks terminarem.
if (op.CalibrandoRuntime && op.TodosModulosMandatoriosCalibrados())
{
op.DefinirEstadoCalibragemRuntime(false);
}
}
}
@ -4224,9 +4140,6 @@ namespace AgroBase.Models
OperacaoLiberacaoDebug = RedisService.GetField<JObject>(CtxKey.DadosOperacao, "operacao_liberacao_debug"),
StatusOperacaoAtual = (StatusOperacao)RedisService.GetField<int>(CtxKey.DadosOperacao, "status", 0),
MotivoStatusOperacao = RedisService.GetField<string>(CtxKey.DadosOperacao, "motivo_status_operacao"),
UltimoMotivoParada = RedisService.GetField<string>(CtxKey.DadosOperacao, "ultimo_motivo_parada"),
ModulosMandatorios = new List<OperacaoModulosMandatoriosModel>(op.Parametros.ModulosMandatorios ?? new List<OperacaoModulosMandatoriosModel>()),
ParametrosMandatorios = new List<OperacaoParametrosMandatoriosModel>(op.Parametros.ParametrosMandatorios ?? new List<OperacaoParametrosMandatoriosModel>()),
DispositivosMapeados = new List<DispositivoDetalhesModel>(SerialService.DispositivosMapeados),
@ -5462,8 +5375,6 @@ namespace AgroBase.Models
public StatusOperacao StatusOperacaoAnterior { get; set; }
public string ErroOperacaoLiberada { get; set; }
public JObject OperacaoLiberacaoDebug { get; set; }
public string MotivoStatusOperacao { get; set; }
public string UltimoMotivoParada { get; set; }
public bool OperacaoConfigurada { get; set; }
public bool OperacaoLiberada { get; set; }
public bool OperacaoIniciada { get; set; }
@ -5528,8 +5439,6 @@ namespace AgroBase.Models
Status = Status,
ErroOperacaoLiberada = ErroOperacaoLiberada,
OperacaoLiberacaoDebug = OperacaoLiberacaoDebug,
MotivoStatusOperacao = MotivoStatusOperacao,
UltimoMotivoParada = UltimoMotivoParada,
OperacaoConfigurada = OperacaoConfigurada,
OperacaoLiberada = OperacaoLiberada,
OperacaoIniciada = OperacaoIniciada,

View File

@ -712,8 +712,6 @@ namespace AgroBase.Models.Operacoes
Iniciada = op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false,
Liberada = op?.Sensoriamento?.Operacao?.OperacaoLiberada ?? false,
Status = op?.Sensoriamento?.Operacao?.StatusOperacaoAtual ?? StatusOperacao.Erro,
UltimoMotivoParada = op?.Sensoriamento?.Operacao?.UltimoMotivoParada,
MotivoStatusOperacao = op?.Sensoriamento?.Operacao?.MotivoStatusOperacao,
Modo = parametros?.Modo ?? ModoOperacao.NaoDefinido,
ImpedimentoErro = op?.Sensoriamento?.Operacao?.ErroOperacaoLiberada,
TempoAguardandoSegs = op?.Sensoriamento?.Operacao?.TempoAguardandoSeg ?? 0,
@ -1499,8 +1497,6 @@ namespace AgroBase.Models.Operacoes
public ModoOperacao Modo { get; set; }
public StatusOperacao Status { get; set; }
public string ImpedimentoErro { get; set; }
public string MotivoStatusOperacao { get; set; }
public string UltimoMotivoParada { get; set; }
public bool Liberada { get; set; }
public bool Iniciada { get; set; }
public bool Emergencia { get; set; }

File diff suppressed because it is too large Load Diff

View File

@ -1084,215 +1084,87 @@ namespace AgroBase.Models
return novoPonto;
}
// V16 - Simulação 4WS unificada com o MPC Python.
//
// Convenção de alto nível:
// anguloControleDeg > 0 => yaw positivo do corpo em qualquer modo.
// Em RodasTraseiras isso implica esterçamento físico traseiro invertido.
//
// Referência cinemática: ponto médio entre os eixos.
// velocidadeMs: módulo da velocidade do centro do rover.
//
// Modelo:
// beta = atan((tan(df) + tan(dr)) / 2)
// kappaGeom = cos(beta) * (tan(df) - tan(dr)) / L
// kappaEf = kappaGeom / (1 + Ku*v²)
// omega = v*kappaEf
//
// Integração analítica com beta e omega constantes no passo.
public static GPSModel SimularNovaPosicao(
TipoMovimentoDirecional tipoMovimento,
double anguloControleDeg,
double velocidadeMs,
double tempoDelta,
double anguloAtualDeg,
GPSModel pos)
public static GPSModel SimularNovaPosicao(TipoMovimentoDirecional tipoMovimento, double anguloControleDeg, double velocidadeMs, double tempoDelta, double anguloAtualDeg, GPSModel pos)
{
double L = Math.Max(0.05, DistanciaEntreEixosCm / 100.0);
// ------------------------------------------------------------
// 1) Comando de alto nível -> ângulos FÍSICOS dos dois eixos
// ------------------------------------------------------------
double dfDeg = 0.0;
double drDeg = 0.0;
double L = DistanciaEntreEixosCm / 100.0;
// Defina deltas por modo
double dfDeg = 0, drDeg = 0;
switch (tipoMovimento)
{
case TipoMovimentoDirecional.RodasDianteiras:
dfDeg = anguloControleDeg;
drDeg = 0.0;
break;
case TipoMovimentoDirecional.RodasTraseiras:
// Importante:
// +angulo de comando significa +yaw do corpo.
// Para obter o mesmo yaw usando apenas o eixo traseiro,
// a roda traseira precisa esterçar fisicamente no sentido oposto.
dfDeg = 0.0;
drDeg = -anguloControleDeg;
break;
case TipoMovimentoDirecional.MovimentoArco:
dfDeg = anguloControleDeg;
drDeg = -anguloControleDeg;
break;
case TipoMovimentoDirecional.MovimentoDiagonal:
case TipoMovimentoDirecional.MovimentoLateral:
// Mesma família cinemática: eixos em fase, sem yaw.
dfDeg = anguloControleDeg;
drDeg = anguloControleDeg;
break;
default:
// Fallback conservador: dianteiras.
dfDeg = anguloControleDeg;
drDeg = 0.0;
break;
case TipoMovimentoDirecional.RodasDianteiras: dfDeg = anguloControleDeg; drDeg = 0; break;
case TipoMovimentoDirecional.RodasTraseiras: dfDeg = 0; drDeg = anguloControleDeg; break;
case TipoMovimentoDirecional.MovimentoArco: dfDeg = anguloControleDeg; drDeg = -anguloControleDeg; break;
case TipoMovimentoDirecional.MovimentoDiagonal: dfDeg = anguloControleDeg; drDeg = anguloControleDeg; break;
}
double df = dfDeg * Math.PI / 180.0;
double dr = drDeg * Math.PI / 180.0;
// Curvatura geométrica
double tf = Math.Tan(dfDeg * Math.PI / 180.0);
double tr = Math.Tan(drDeg * Math.PI / 180.0);
if (Math.Abs(tf) < 1e-6) tf = 0;
if (Math.Abs(tr) < 1e-6) tr = 0;
double tf = Math.Tan(df);
double tr = Math.Tan(dr);
double kappa = 0; // 1/m
if (tipoMovimento == TipoMovimentoDirecional.RodasDianteiras) kappa = tf / L;
else if (tipoMovimento == TipoMovimentoDirecional.RodasTraseiras) kappa = tr / L;
else if (tipoMovimento == TipoMovimentoDirecional.MovimentoArco) kappa = (tf - tr) / L;
else /*Diagonal,Lateral*/ kappa = 0;
if (Math.Abs(tf) < 1e-12) tf = 0.0;
if (Math.Abs(tr) < 1e-12) tr = 0.0;
// Correção dinâmica (opcional)
double Ku = 2.5; // ajuste fino em campo
double kappaEf = kappa / (1 + Ku * velocidadeMs * velocidadeMs);
// ------------------------------------------------------------
// 2) Cinemática 4WS no ponto médio entre eixos
// ------------------------------------------------------------
double beta = Math.Atan(0.5 * (tf + tr));
// velocidadeMs é o módulo da velocidade do centro.
// Por isso entra cos(beta) na curvatura de yaw por metro percorrido.
double kappaGeom =
Math.Cos(beta) * (tf - tr) / L;
if (Math.Abs(kappaGeom) < 1e-12)
kappaGeom = 0.0;
// ------------------------------------------------------------
// 3) Correção dinâmica
// ------------------------------------------------------------
// Deve ser o MESMO valor usado em:
// mpc_v16 -> cinematica_4ws_ku
double Ku = 2.5;
double kappaEf =
kappaGeom /
(1.0 + Ku * velocidadeMs * velocidadeMs);
double omega = velocidadeMs * kappaEf; // rad/s
// ------------------------------------------------------------
// 4) Integração analítica
// ------------------------------------------------------------
// Convenção:
// theta = heading do corpo, 0=Norte, +90°=Leste.
// A velocidade do centro aponta para theta + beta.
double theta0 = anguloAtualDeg * Math.PI / 180.0;
// Integração
double theta = anguloAtualDeg * Math.PI / 180.0;
double omega = velocidadeMs * kappaEf; // rad/s
double dtheta = omega * tempoDelta;
double theta1 = theta0 + dtheta;
theta += dtheta;
double phi0 = theta0 + beta;
double heading = theta;
if (new List<TipoMovimentoDirecional>() { TipoMovimentoDirecional.MovimentoDiagonal, TipoMovimentoDirecional.MovimentoLateral }.Contains(tipoMovimento))
heading = theta + dfDeg * Math.PI / 180.0; // crab: desloca na direção do steering
double dx;
double dy;
if (Math.Abs(omega) <= 1e-9)
{
double distancia = velocidadeMs * tempoDelta;
dx = Math.Sin(phi0) * distancia; // Leste(+)
dy = Math.Cos(phi0) * distancia; // Norte(+)
}
else
{
double phi1 = theta1 + beta;
double raioVel = velocidadeMs / omega;
dx =
raioVel *
(Math.Cos(phi0) - Math.Cos(phi1));
dy =
raioVel *
(Math.Sin(phi1) - Math.Sin(phi0));
}
// >>> mesma convenção do Python: 0=Norte
double dx = velocidadeMs * tempoDelta * Math.Sin(heading); // Leste(+)
double dy = velocidadeMs * tempoDelta * Math.Cos(heading); // Norte(+)
GPSModel ultimaPosicao = GPSService.historicoPosicao.Peek();
if (pos == null)
return ultimaPosicao;
if (pos == null) return ultimaPosicao;
// ------------------------------------------------------------
// 5) Plano local -> geográfico
// ------------------------------------------------------------
// geo
double R_earth = GPSUtils.RaioDaTerra;
double dLat =
(dy / R_earth) *
180.0 / Math.PI;
double cosLat =
Math.Cos(pos.LatitudeAnt * Math.PI / 180.0);
// Proteção só para evitar divisão numérica absurda.
if (Math.Abs(cosLat) < 1e-9)
cosLat = cosLat >= 0 ? 1e-9 : -1e-9;
double dLon =
(dx / (R_earth * cosLat)) *
180.0 / Math.PI;
double dLat = (dy / R_earth) * 180.0 / Math.PI;
double dLon = (dx / (R_earth * Math.Cos(pos.Latitude * Math.PI / 180.0))) * 180.0 / Math.PI;
double latitude = pos.LatitudeAnt + dLat;
double longitude = pos.LongitudeAnt + dLon;
DateTime agora = DateTime.Now;
double agoraMono =
Stopwatch.GetTimestamp() /
(double)Stopwatch.Frequency;
double agoraMono = Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
double orientacaoRealDeg =
GPSUtils.NormalizarAngulo(
theta1 * 180.0 / Math.PI
);
// Direção instantânea de deslocamento do centro no final do passo.
double orientacaoMovimentoDeg =
GPSUtils.NormalizarAngulo(
(theta1 + beta) * 180.0 / Math.PI
);
double orientacaoRealDeg = GPSUtils.NormalizarAngulo(theta * 180.0 / Math.PI);
double orientacaoMovimentoDeg = GPSUtils.NormalizarAngulo(heading * 180.0 / Math.PI);
GPSModel novaPosicao = new GPSModel
{
Momento = agora,
DataHora = agora,
UltimoComandoRespondido = agora,
Lat0 = GPSService.UltimaLeitura.Lat0,
Lon0 = GPSService.UltimaLeitura.Lon0,
Lat0 = GPSService.UltimaLeitura.Lat0,
Lon0 = GPSService.UltimaLeitura.Lon0,
LatitudeAnt = latitude,
LongitudeAnt = longitude,
OrientacaoReal = orientacaoRealDeg,
AnguloCarroDefinido = orientacaoRealDeg,
OrientacaoMovimento = orientacaoMovimentoDeg,
TipoOrientacao = "A",
Distancia = velocidadeMs * tempoDelta,
Velocidade = velocidadeMs,
Heartbeat = ultimaPosicao.Heartbeat + 1,
TimestampOri = ultimaPosicao.TimestampOri.Clone(),
TimestampPos = ultimaPosicao.TimestampPos.Clone(),
};
// Mantém exatamente o lever-arm já usado no simulador real.
var corrigida =
GPSService.LeverArm.FixLeverArmLatLon_Fast(
novaPosicao.LatitudeAnt,
@ -1300,13 +1172,16 @@ namespace AgroBase.Models
novaPosicao.OrientacaoReal,
5
);
novaPosicao.Latitude = corrigida.lat;
novaPosicao.Longitude = corrigida.lon;
//(double latCor, double lonCorr) = GeoLeverArm.FixLeverArmLatLon_Fast(latitude, longitude, novaPosicao.OrientacaoReal);
//novaPosicao.Latitude = latCor;
//novaPosicao.Longitude = lonCorr;
novaPosicao.TimestampOri.valor = agoraMono;
novaPosicao.TimestampPos.valor = agoraMono;
return novaPosicao;
}

View File

@ -576,16 +576,10 @@ class CameraMultispectral:
)
try:
if stream is not None:
fechar_stream = getattr(stream, "fechar", None)
if not callable(fechar_stream):
fechar_stream = getattr(stream, "parar", None)
if callable(fechar_stream):
fechar_stream()
except Exception as e:
self.mostrar_log(
f"[CameraMultispectral] Erro ao fechar stream TCP: {e}"
)
if stream is not None and hasattr(stream, "parar"):
stream.parar()
except Exception:
pass
with self._lock:
self.ultimo_tensor_multispec = None

View File

@ -223,19 +223,6 @@ class CameraOak:
self.rodando = False
self.iniciado = False
stream = self.stream
self.stream = None
try:
if stream is not None:
fechar_stream = getattr(stream, "fechar", None)
if not callable(fechar_stream):
fechar_stream = getattr(stream, "parar", None)
if callable(fechar_stream):
fechar_stream()
except Exception as e:
self.mostrar_log(f"Erro ao fechar stream TCP da câmera: {e}")
try:
if self._cache_thread is not None and self._cache_thread.is_alive():
self._cache_thread.join(timeout=1.0)
@ -295,6 +282,10 @@ class CameraOak:
"frame_valido": False,
}
try:
self.stream = None
except Exception:
pass
def _is_erro_fatal_depthai(self, erro):
if erro is None:

View File

@ -208,8 +208,6 @@ class OakFcc3Manager:
"dropped_queue": 0,
"last_error": None,
"last_loop_ms": 0.0,
"last_packet_age_ms": None,
"_last_frame_seen_perf_by_cam": {},
}
def __enter__(self):
@ -1635,15 +1633,6 @@ class OakFcc3Manager:
t0_get = self._cap_now_ms()
msg = q.get()
try:
seen = self._capture_thread_stats.setdefault(
"_last_frame_seen_perf_by_cam", {}
)
seen[cam_id] = time.perf_counter()
except Exception:
pass
if perf is not None:
perf["drain_get_msg_ms"] += self._cap_now_ms() - t0_get
@ -2354,7 +2343,6 @@ class OakFcc3Manager:
"last_error": None,
"last_loop_ms": 0.0,
"last_packet_age_ms": None,
"_last_frame_seen_perf_by_cam": {},
}
def _start_async_capture_thread(self):
@ -2429,11 +2417,6 @@ class OakFcc3Manager:
# Importante: esta thread é a única que mexe nas queues/buffers.
self._drain_queues_to_buffers(perf=perf)
if perf is not None:
perf["buffer_lengths_after_drain"] = (
self._buffer_lengths_snapshot()
)
# Em RAW_BRUTO + async, o produtor publica apenas descritores
# ImgFrame da tripleta sincronizada. O consumidor materializa
# somente o pacote latest que realmente pegar. Isso elimina
@ -2449,25 +2432,6 @@ class OakFcc3Manager:
)
if synced is None:
# Guarda diagnóstico do motivo pelo qual o sincronizador
# ainda não conseguiu formar um pacote. Isso aparece no
# status usado pelo watchdog do WeedWorker e permite saber
# se uma cabeça específica ficou sem frames ou se houve
# descarte por sincronismo estrito.
if perf is not None:
self._capture_thread_stats["last_wait_reason"] = (
perf.get("wait_reason")
)
self._capture_thread_stats["last_buffer_lengths"] = dict(
perf.get("buffer_lengths_after_drain") or {}
)
self._capture_thread_stats["last_drained_by_cam"] = dict(
perf.get("drained_by_cam") or {}
)
self._capture_thread_stats["last_queue_has_true_by_cam"] = dict(
perf.get("queue_has_true_by_cam") or {}
)
# Dorme curto. Pode testar 0.0005 se quiser reduzir latência.
time.sleep(0.001)
continue
@ -2539,10 +2503,6 @@ class OakFcc3Manager:
self._capture_thread_stats["packets"] += 1
self._capture_thread_stats["last_error"] = None
self._capture_thread_stats["last_wait_reason"] = "packet_ready"
self._capture_thread_stats["last_buffer_lengths"] = (
self._buffer_lengths_snapshot()
)
self._capture_thread_stats["last_loop_ms"] = (time.perf_counter() - t_loop0) * 1000.0
self._capture_cond.notify_all()
@ -2590,20 +2550,6 @@ class OakFcc3Manager:
latest_age_ms = (time.perf_counter() - float(self._latest_packet.get("created_perf_counter", 0.0))) * 1000.0
st = dict(getattr(self, "_capture_thread_stats", {}) or {})
last_seen_perf = st.pop(
"_last_frame_seen_perf_by_cam", {}
) or {}
now_perf = time.perf_counter()
camera_frame_age_ms = {
str(cam_id): round(
max(0.0, now_perf - float(ts)) * 1000.0,
1,
)
for cam_id, ts in last_seen_perf.items()
if ts is not None
}
st.update({
"enabled": bool(getattr(self, "async_capture_enabled", True)),
"mode": str(getattr(self, "async_capture_mode", "latest")),
@ -2613,8 +2559,5 @@ class OakFcc3Manager:
"last_consumed_seq": int(getattr(self, "_last_consumed_packet_seq", 0)),
"queue_len": int(len(getattr(self, "_packet_queue", []))),
"latest_age_ms": latest_age_ms,
"last_packet_age_ms": latest_age_ms,
"buffer_lengths": self._buffer_lengths_snapshot(),
"camera_frame_age_ms": camera_frame_age_ms,
})
return st

View File

@ -1572,10 +1572,6 @@ class CameraTcpStreamer:
stream_fps=0.0,
)
def parar(self):
"""Alias de compatibilidade para encerramento explícito."""
self.fechar()
def __del__(self):
try:
self.fechar()

View File

@ -1651,25 +1651,14 @@ class CameraManager:
threading.Thread(target=loop, daemon=True).start()
def _iniciar_loop_frame_stream(self, freq=2.0):
"""
Alimenta o CameraTcpStreamer sem duplicar a política de FPS.
O streamer é a única camada responsável por throttle, banda,
backpressure e reconexão. Este produtor apenas oferece o último frame
em frequência superior ao maior FPS operacional.
"""
producer_hz = max(10.0, float(freq or 2.0) * 2.0)
def loop():
while True:
t_wall0 = time.time()
t_loop0 = time.perf_counter()
stream_on = False
try:
camera_atual = self.camera
if camera_atual is None:
time.sleep(0.25)
if self.camera is None:
time.sleep(0.5)
continue
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
@ -1684,7 +1673,7 @@ class CameraManager:
frame = self._get_cached_preview_frame(frame_type, copiar=False)
if frame is not None:
camera_atual.enviar_frame_tcp(frame)
self.camera.enviar_frame_tcp(frame)
if self.debug_visual and self._visual_disponivel_para_frame():
self._solicitar_preview(TipoFrameCamera.Debug)
@ -1703,14 +1692,15 @@ class CameraManager:
finally:
lat = time.time() - t_wall0
hz = producer_hz if stream_on else 2.0
time.sleep(max(0.0, (1.0 / hz) - lat))
threading.Thread(
target=loop,
daemon=True,
name="visual-stream-feed",
).start()
try:
freq_atual = float(getattr(self.camera.stream, "_op_fps", freq) or freq)
except Exception:
freq_atual = freq
time.sleep(max(0.0, (1.0 / max(freq_atual, 0.1)) - lat))
threading.Thread(target=loop, daemon=True).start()
def _iniciar_loop_analise_continua(self, freq=8.0):
def loop():

View File

@ -3231,26 +3231,14 @@ class CameraManager:
threading.Thread(target=loop, daemon=True).start()
def _iniciar_loop_frame_stream(self, freq=2.0):
"""
Alimenta o CameraTcpStreamer com o último preview disponível.
Importante: o controle de FPS/banda existe SOMENTE dentro do
CameraTcpStreamer. Este loop roda um pouco mais rápido que o maior
FPS operacional para não criar um segundo gate sincronizado que possa
pular janelas inteiras por jitter de agendamento.
"""
producer_hz = max(10.0, float(freq or 2.0) * 2.0)
def loop():
while True:
t0 = time.time()
t_loop0 = time.perf_counter()
stream_on = False
try:
camera_atual = self.camera
if camera_atual is None:
time.sleep(0.25)
if self.camera is None:
time.sleep(5.0)
continue
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
@ -3260,11 +3248,9 @@ class CameraManager:
frame_type = TipoFrameCamera(
camera_ctx.get("frame_type", TipoFrameCamera.Rgb.value)
)
# Stream é apenas consumidor do cache pronto.
# Stream também é consumidor de cache. Não monta preview.
frame = self._get_cached_preview_frame_ref(frame_type)
if frame is not None:
camera_atual.enviar_frame_tcp(frame)
self.camera.enviar_frame_tcp(frame)
if self.debug_visual:
dbg = self._get_cached_preview_frame_ref(TipoFrameCamera.Debug)
@ -3284,14 +3270,15 @@ class CameraManager:
)
lat = time.time() - t0
hz = producer_hz if stream_on else 2.0
time.sleep(max(0.0, (1.0 / hz) - lat))
threading.Thread(
target=loop,
daemon=True,
name="weed-stream-feed",
).start()
try:
freq_atual = float(getattr(self.camera.stream, "_op_fps", freq) or freq)
except Exception:
freq_atual = freq
time.sleep(max(0.0, (1.0 / freq_atual) - lat))
threading.Thread(target=loop, daemon=True).start()
def _iniciar_loop_analise_continua(self, freq=15.0):
def loop():

View File

@ -537,7 +537,6 @@ namespace OperationControl.ViewModels
public void AtualizarBarraSuperior(OperacaoParametrosModel rover)
{
var obj = rover?.DadosLeitura;
if (rover == null || obj == null)
return;
@ -546,76 +545,12 @@ namespace OperationControl.ViewModels
var eventos = new List<MotivoTopBarModel>();
// ============================================================
// Helpers
// ============================================================
string NormalizarTexto(string texto)
{
if (string.IsNullOrWhiteSpace(texto))
return null;
var partes = texto
.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x?.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList();
return partes.Count > 0
? string.Join(" • ", partes)
: null;
}
bool EhModuloMandatorio(T_Code modulo)
{
// A telemetria recebida do rover é a fonte principal.
// Só cai para a configuração local caso o snapshot não exista.
var modulosMandatorios =
obj.Operacao?.ModulosMandatorios
?? rover.ModulosMandatorios;
return modulosMandatorios?.Any(x =>
(x?.Dispositivo ?? T_Code.Vzo) == modulo &&
(x?.Utilizar ?? false) &&
(x?.Mandatorio ?? false)
) ?? false;
}
string MotivoOperacaoAtual()
{
// 1) Motivo autoritativo de NÃO liberação.
var motivo = NormalizarTexto(obj.Operacao?.ImpedimentoErro);
if (!string.IsNullOrWhiteSpace(motivo))
return motivo;
// 2) Motivo do estado/status atual.
motivo = NormalizarTexto(obj.Operacao?.MotivoStatusOperacao);
if (!string.IsNullOrWhiteSpace(motivo))
return motivo;
// 3) Último motivo de parada apenas como fallback.
motivo = NormalizarTexto(obj.Operacao?.UltimoMotivoParada);
if (!string.IsNullOrWhiteSpace(motivo))
return motivo;
return "A operação não foi liberada pelo rover.";
}
// ============================================================
// 0) Comunicação
// ============================================================
double tempoSemResposta =
(DateTime.Now - rover.UltimoContato).TotalSeconds;
bool semComunicacao =
!AppShell.Mock &&
tempoSemResposta >= VariaveisControleOperacao.TempoRoverVivo;
double tempoSemResposta = (DateTime.Now - rover.UltimoContato).TotalSeconds;
bool semComunicacao = !AppShell.Mock && tempoSemResposta >= VariaveisControleOperacao.TempoRoverVivo;
// ============================
// 0) Perda de comunicação
// ============================
if (semComunicacao)
{
eventos.Add(new MotivoTopBarModel
@ -623,116 +558,63 @@ namespace OperationControl.ViewModels
Prioridade = 2000,
Fonte = "Comunicacao",
Titulo = "PERDA DE COMUNICAÇÃO COM O ROVER",
Descricao =
$"Sem resposta do rover há {tempoSemResposta:F1} s. " +
"Tentando reconectar automaticamente.",
Descricao = $"Sem resposta do rover há {tempoSemResposta:F1} s. Tentando reconectar automaticamente.",
StatusVisual = StatusModulo.Desconectado
});
}
// ============================================================
// 1) NPC / núcleo central
// ============================================================
bool todosModulosDesconectados =
obj.ModulosSaude?.Any() == true &&
obj.ModulosSaude.All(x =>
(x?.status ?? StatusModulo.Desconectado) ==
StatusModulo.Desconectado);
if (todosModulosDesconectados)
// ============================
// 0) NCP desconectado
// ============================
if (obj.ModulosSaude?.All(x => (x?.status ?? StatusModulo.Desconectado) == StatusModulo.Desconectado) ?? false)
{
eventos.Add(new MotivoTopBarModel
{
Prioridade = 1100,
Fonte = "NCP",
Titulo = "NÚCLEO CENTRAL DE PROCESSAMENTO DESCONECTADO",
Descricao =
"O núcleo central de processamento foi desconectado. " +
"Tentando reconectar...",
Descricao = "O núcleo central de processamento foi desconectado, tentando reconectar...",
StatusVisual = StatusModulo.Falha
});
}
// ============================================================
// 2) Emergência
// ============================================================
// ============================
// 1) Emergência
// ============================
if (obj.Operacao?.Emergencia == true)
{
var motivo =
NormalizarTexto(obj.Operacao.MotivoStatusOperacao)
?? "Parada de emergência solicitada.";
eventos.Add(new MotivoTopBarModel
{
Prioridade = 1000,
Fonte = "Emergencia",
Titulo = "OPERAÇÃO BLOQUEADA POR EMERGÊNCIA",
Descricao = motivo,
Descricao = "Parada de emergência solicitada.",
StatusVisual = StatusModulo.Falha
});
}
// ============================================================
// 3) Pausa
// ============================================================
// ============================
// 2) Pausa
// ============================
if (obj.Operacao?.Pausa == true)
{
var motivo =
NormalizarTexto(obj.Operacao.MotivoStatusOperacao)
?? "Pausa operacional solicitada.";
eventos.Add(new MotivoTopBarModel
{
Prioridade = 900,
Fonte = "Pausa",
Titulo = "OPERAÇÃO PAUSADA",
Descricao = motivo,
Descricao = "Pausa operacional solicitada.",
StatusVisual = StatusModulo.Alerta
});
}
// ============================================================
// 4) Operação não liberada
//
// Essa é a informação autoritativa vinda do rover.
// Exemplo:
// Sen: Desconectado
// Gps: Falha
// Parâmetro obrigatório ausente
// ============================================================
if (
obj.Operacao != null &&
!obj.Operacao.Liberada &&
obj.Operacao.Pausa != true &&
obj.Operacao.Emergencia != true
)
{
eventos.Add(new MotivoTopBarModel
{
Prioridade = 850,
Fonte = "Operacao",
Titulo = "OPERAÇÃO BLOQUEADA",
Descricao = MotivoOperacaoAtual(),
StatusVisual = StatusModulo.Falha
});
}
// ============================================================
// 5) Controle
// ============================================================
var motivosControle =
obj.Controle?.Motivos?
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(NormalizarTexto)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList()
?? new List<string>();
// ============================
// 3) Controle
// ============================
var motivosControle = obj.Controle?.Motivos?
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList() ?? new List<string>();
foreach (var motivo in motivosControle)
{
@ -746,18 +628,13 @@ namespace OperationControl.ViewModels
});
}
// ============================================================
// 6) Trajetória / autonomia
// ============================================================
var motivosTrajetoria =
obj.Trajetoria?.AutonomiaCorredor?.Motivos?
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(NormalizarTexto)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList()
?? new List<string>();
// ============================
// 4) Trajetória / autonomia
// ============================
var motivosTrajetoria = obj.Trajetoria?.AutonomiaCorredor?.Motivos?
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList() ?? new List<string>();
foreach (var motivo in motivosTrajetoria)
{
@ -771,215 +648,110 @@ namespace OperationControl.ViewModels
});
}
// ============================================================
// 7) Estado operacional
//
// Aqui pegamos estados legítimos em que a operação está
// liberada, porém ainda não está executando.
//
// Exemplos:
// Aguardando retomada segura
// Parametrizando
// Finalizando
// ============================================================
if (
obj.Operacao != null &&
obj.Operacao.Liberada &&
obj.Operacao.Pausa != true &&
obj.Operacao.Emergencia != true &&
obj.Operacao.Status != StatusOperacao.EmAndamento &&
!string.IsNullOrWhiteSpace(obj.Operacao.MotivoStatusOperacao)
)
{
var motivo =
NormalizarTexto(obj.Operacao.MotivoStatusOperacao);
eventos.Add(new MotivoTopBarModel
{
Prioridade = 650,
Fonte = "StatusOperacao",
Titulo =
$"OPERAÇÃO {obj.Operacao.Status.ToString().ToUpper()}",
Descricao = motivo,
StatusVisual =
obj.Operacao.Status == StatusOperacao.Erro
? StatusModulo.Falha
: StatusModulo.Alerta
});
}
// ============================================================
// 8) Módulos críticos
// ============================================================
var modulosCriticos =
obj.ModulosSaude?
.Where(mod =>
{
if (mod == null)
return false;
var status =
mod?.status ?? StatusModulo.Desconectado;
bool temCondicaoSevera =
mod?.condicoes_operacionais?.Any(
cond => cond?.severidade > 90
) ?? false;
bool mandatorio =
EhModuloMandatorio(mod?.modulo ?? T_Code.Vzo);
bool naoOperante =
!new[]
{
StatusModulo.Operante,
StatusModulo.Alerta
}.Contains(status);
bool emAlerta =
status == StatusModulo.Alerta;
return
temCondicaoSevera ||
(mandatorio && naoOperante) ||
(mandatorio && emAlerta);
})
.ToList()
?? new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>();
// ============================
// 5) Módulos com condições operacionais críticas
// ============================
var modulosCriticos = obj.ModulosSaude?
.Where(mod =>
(mod?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false) ||
(
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(mod?.status ?? StatusModulo.Desconectado) &&
(rover?.ModulosMandatorios?.Any(x =>
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) &&
(x?.Utilizar ?? false) &&
(x?.Mandatorio ?? false)
) ?? false)
) ||
(
(mod?.status ?? StatusModulo.Desconectado) == StatusModulo.Alerta &&
(rover?.ModulosMandatorios?.Any(x =>
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) &&
(x?.Utilizar ?? false) &&
(x?.Mandatorio ?? false)
) ?? false)
)
)
.ToList() ?? new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>();
foreach (var modulo in modulosCriticos)
{
var codigoModulo =
modulo?.modulo ?? T_Code.Vzo;
var nomeModulo =
codigoModulo.ToString();
var status =
modulo?.status ?? StatusModulo.Desconectado;
bool mandatorio =
EhModuloMandatorio(codigoModulo);
bool temCondicaoSevera =
modulo?.condicoes_operacionais?.Any(
cond => cond?.severidade > 90
) ?? false;
var nomeModulo = (modulo?.modulo ?? T_Code.Vzo).ToString();
bool temCondicaoSevera = modulo?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false;
bool moduloMandatorioNaoOperante =
mandatorio &&
status != StatusModulo.Operante &&
status != StatusModulo.Alerta;
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(modulo?.status ?? StatusModulo.Desconectado) &&
(rover?.ModulosMandatorios?.Any(x =>
(x?.Dispositivo ?? T_Code.Vzo) == (modulo?.modulo ?? T_Code.Vzo) &&
(x?.Utilizar ?? false) &&
(x?.Mandatorio ?? false)
) ?? false);
bool moduloMandatorioAlerta =
mandatorio &&
status == StatusModulo.Alerta;
// --------------------------------------------------------
// 8.1) Mandatório não operacional
// --------------------------------------------------------
(modulo?.status ?? StatusModulo.Desconectado) == StatusModulo.Alerta &&
(rover?.ModulosMandatorios?.Any(x =>
(x?.Dispositivo ?? T_Code.Vzo) == (modulo?.modulo ?? T_Code.Vzo) &&
(x?.Utilizar ?? false) &&
(x?.Mandatorio ?? false)
) ?? false);
// Caso 1: módulo mandatório não operante
if (moduloMandatorioNaoOperante)
{
List<string> motivos =
modulo?.motivos?
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(NormalizarTexto)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList()
?? new List<string>();
string motivoTexto =
motivos.Count > 0
? string.Join("; ", motivos)
: "Sem motivo detalhado informado.";
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
string motivos = string.Join("; ", modulo?.motivos ?? new List<string>() { "Desconhecido" });
eventos.Add(new MotivoTopBarModel
{
Prioridade = 620,
Fonte = "ModuloCritico",
Titulo = "MÓDULO MANDATÓRIO NÃO OPERACIONAL",
Descricao =
$"{nomeModulo}: módulo mandatório em estado " +
$"'{status}': {motivoTexto}",
Descricao = $"{nomeModulo}: módulo mandatório em estado '{statusTexto}': {motivos}",
StatusVisual = StatusModulo.Falha,
Modulo = codigoModulo
Modulo = modulo?.modulo
});
}
// --------------------------------------------------------
// 8.2) Condições operacionais severas
// --------------------------------------------------------
// Caso 2: condições operacionais severas
if (temCondicaoSevera)
{
var descricoesCriticas =
modulo?.condicoes_operacionais?
.Where(cond => cond?.severidade > 90)
.Select(cond =>
NormalizarTexto(cond?.descricao))
.Where(desc =>
!string.IsNullOrWhiteSpace(desc))
.Distinct()
.ToList()
?? new List<string>();
var descricoesCriticas = modulo?.condicoes_operacionais?
.Where(cond => cond?.severidade > 90)
.Select(cond => cond?.descricao ?? "")
.Where(desc => !string.IsNullOrWhiteSpace(desc))
.Distinct()
.ToList() ?? new List<string>();
foreach (var descricao in descricoesCriticas)
foreach (var desc in descricoesCriticas)
{
eventos.Add(new MotivoTopBarModel
{
Prioridade = 610,
Fonte = "ModuloCritico",
Titulo = "CONDIÇÕES OPERACIONAIS CRÍTICAS",
Descricao =
$"{nomeModulo}: {descricao}",
Descricao = $"{nomeModulo}: {desc}",
StatusVisual = StatusModulo.Alerta,
Modulo = codigoModulo
Modulo = modulo?.modulo
});
}
}
// --------------------------------------------------------
// 8.3) Mandatório em alerta
// --------------------------------------------------------
// Caso 3: módulo mandatório em alerta
if (moduloMandatorioAlerta)
{
List<string> motivos =
modulo?.motivos?
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(NormalizarTexto)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList()
?? new List<string>();
string motivoTexto =
motivos.Count > 0
? string.Join("; ", motivos)
: "Sem motivo detalhado informado.";
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
string motivos = string.Join("; ", modulo?.motivos ?? new List<string>() { "Desconhecido" });
eventos.Add(new MotivoTopBarModel
{
Prioridade = 600,
Fonte = "ModuloCritico",
Titulo = "MÓDULO MANDATÓRIO EM ALERTA",
Descricao =
$"{nomeModulo}: módulo mandatório em estado " +
$"'{status}': {motivoTexto}",
Descricao = $"{nomeModulo}: módulo mandatório em estado '{statusTexto}': {motivos}",
StatusVisual = StatusModulo.Alerta,
Modulo = codigoModulo
Modulo = modulo?.modulo
});
}
}
// ============================================================
// 9) Ordenação dos módulos
// ============================================================
var ordemModulos = new Dictionary<T_Code, int>
{
{ T_Code.Ipb, 1 },
@ -991,38 +763,17 @@ namespace OperationControl.ViewModels
{ T_Code.Sen, 7 }
};
// ============================================================
// 10) Limpeza / ordenação dos eventos
// ============================================================
var eventosOrdenados =
eventos
.Where(x =>
!string.IsNullOrWhiteSpace(x.Titulo) &&
!string.IsNullOrWhiteSpace(x.Descricao))
.GroupBy(x => new
{
x.Fonte,
x.Titulo,
x.Descricao,
x.Prioridade,
x.StatusVisual
})
.Select(g => g.First())
.OrderByDescending(x => x.Prioridade)
.ThenBy(x =>
x.Modulo.HasValue &&
ordemModulos.TryGetValue(
x.Modulo.Value,
out var ordem)
? ordem
: int.MaxValue)
.ThenBy(x => x.Fonte)
.ToList();
// ============================================================
// 11) Estado normal
// ============================================================
// ============================
// 6) Remove vazios / repetidos
// ============================
var eventosOrdenados = eventos
.Where(x => !string.IsNullOrWhiteSpace(x.Titulo) && !string.IsNullOrWhiteSpace(x.Descricao))
.GroupBy(x => new { x.Fonte, x.Titulo, x.Descricao, x.Prioridade, x.StatusVisual })
.Select(g => g.First())
.OrderByDescending(x => x.Prioridade)
.ThenBy(x => x.Modulo.HasValue && ordemModulos.ContainsKey(x.Modulo.Value)? ordemModulos[x.Modulo.Value] : int.MaxValue)
.ThenBy(x => x.Fonte)
.ToList();
if (!eventosOrdenados.Any())
{
@ -1031,163 +782,96 @@ namespace OperationControl.ViewModels
Prioridade = 0,
Fonte = "Normal",
Titulo = "ROVER OPERANDO NORMALMENTE",
Descricao =
"Trajetória, controle e operação em condição normal.",
StatusVisual =
obj.StatusRover == StatusModulo.Desconectado
? StatusModulo.Desconectado
: StatusModulo.Operante
Descricao = "Trajetória, controle e operação em condição normal.",
StatusVisual = obj.StatusRover == StatusModulo.Desconectado
? StatusModulo.Desconectado
: StatusModulo.Operante
});
}
// ============================================================
// 12) Montagem visual
// ============================================================
var principal =
eventosOrdenados.First();
StatusModulo statusVisual =
principal.StatusVisual;
string linha1 =
principal.Titulo;
// ============================
// 7) Monta visual final
// ============================
StatusModulo statusVisual;
string linha1;
string linha2;
string badge;
string resumo;
// Se perdeu comunicação, não exibimos diagnóstico antigo
// recebido antes da queda junto com a perda de comunicação.
if (principal.Fonte == "Comunicacao")
if (eventosOrdenados.Any())
{
linha2 = principal.Descricao;
var principal = eventosOrdenados.First();
statusVisual = principal.StatusVisual;
linha1 = principal.Titulo;
// Pega até 3 descrições distintas, por prioridade
var descricoes = eventosOrdenados
.Select(x => x.Descricao?.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.Take(3)
.ToList();
linha2 = string.Join(" • ", descricoes);
// Badge curto
badge = principal.Fonte switch
{
"Comunicacao" => "DESCONECTADO",
"Emergencia" => "EMERGÊNCIA",
"Pausa" => "PAUSADO",
"Trajetoria" => "TRAJETÓRIA",
"Controle" => "CONTROLE",
"ModuloCritico" => "ALERTA",
_ => statusVisual.ToString().ToUpper()
};
// Resumo curto
resumo = principal.Fonte switch
{
"Comunicacao" => "Sem telemetria",
"Emergencia" => "Parada imediata",
"Pausa" => "Aguardando retomada",
"Trajetoria" => "Operação bloqueada",
"Controle" => "Controle bloqueado",
"ModuloCritico" => "Atenção operacional",
_ => (obj.Operacao?.Status ?? StatusOperacao.NaoIniciado).ToString()
};
}
else
{
var descricoes =
eventosOrdenados
.Select(x => x.Descricao?.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.Take(3)
.ToList();
statusVisual = obj.StatusRover == StatusModulo.Desconectado
? StatusModulo.Desconectado
: StatusModulo.Operante;
linha2 =
string.Join(" • ", descricoes);
linha1 = "ROVER OPERANDO NORMALMENTE";
linha2 = "Trajetória, controle e operação em condição normal.";
badge = statusVisual.ToString().ToUpper();
resumo = (obj.Operacao?.Status ?? StatusOperacao.NaoIniciado).ToString();
}
string badge =
principal.Fonte switch
{
"Comunicacao" => "DESCONECTADO",
"NCP" => "DESCONECTADO",
"Emergencia" => "EMERGÊNCIA",
"Pausa" => "PAUSADO",
"Operacao" => "BLOQUEADO",
"Controle" => "CONTROLE",
"Trajetoria" => "TRAJETÓRIA",
"StatusOperacao"
=> obj.Operacao?.Status
.ToString()
.ToUpper() ?? "OPERAÇÃO",
"ModuloCritico" => "ALERTA",
"Normal" => statusVisual
.ToString()
.ToUpper(),
_ => statusVisual
.ToString()
.ToUpper()
};
string resumo =
principal.Fonte switch
{
"Comunicacao" =>
"Sem telemetria",
"NCP" =>
"Núcleo central desconectado",
"Emergencia" =>
"Parada imediata",
"Pausa" =>
"Aguardando retomada",
"Operacao" =>
"Aguardando liberação",
"Controle" =>
"Controle bloqueado",
"Trajetoria" =>
"Trajetória bloqueada",
"StatusOperacao" =>
NormalizarTexto(
obj.Operacao?.MotivoStatusOperacao)
?? obj.Operacao?.Status.ToString()
?? "Estado operacional",
"ModuloCritico" =>
"Atenção operacional",
"Normal" =>
obj.Operacao?.Status.ToString()
?? StatusOperacao.NaoIniciado.ToString(),
_ =>
obj.Operacao?.Status.ToString()
?? StatusOperacao.NaoIniciado.ToString()
};
// ============================================================
// 13) Histórico
// ============================================================
_viewOperacaoCenter?
.Historico?
._vm?
.CompararERegistrarMudancasEventos(
rover.RoverId,
eventosOrdenados);
// ============================================================
// 14) Segurança visual
// ============================================================
if (string.IsNullOrWhiteSpace(linha1))
linha1 = "ESTADO DO ROVER";
_viewOperacaoCenter?.Historico?._vm?.CompararERegistrarMudancasEventos(rover.RoverId, eventosOrdenados);
// Segurança extra: evita linha vazia
if (string.IsNullOrWhiteSpace(linha2))
linha2 = "Sem detalhes adicionais.";
if (string.IsNullOrWhiteSpace(badge))
badge = "STATUS";
if (string.IsNullOrWhiteSpace(resumo))
resumo =
obj.Operacao?.Status.ToString()
?? StatusOperacao.NaoIniciado.ToString();
// ============================================================
// 15) Atualização da UI
// ============================================================
System.Windows.Application.Current.Dispatcher.BeginInvoke(
new Action(() =>
{
_viewOperacaoTop?._vm?.AtualizarStatus(
statusVisual,
rover.RoverId,
obj.Operacao?.Status
?? StatusOperacao.NaoIniciado,
linha1,
linha2,
badge,
resumo
);
})
);
// ============================
// 8) Atualiza UI
// ============================
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
_viewOperacaoTop?._vm?.AtualizarStatus(
statusVisual,
rover.RoverId,
obj?.Operacao?.Status ?? StatusOperacao.NaoIniciado,
linha1,
linha2,
badge,
resumo
);
}));
}
public void AtualizarBarraSuperior(StatusModulo status, string titulo, string linha1, string linha2, string status_str, string resumo)