Compare commits

...

2 Commits

Author SHA1 Message Date
Diego Freitas b737b11a59 ajuste no stream de video, alinhamento do carro no corredor, manobras, ihm, simulação cinemática, 2026-09-17 23:10:59 -03:00
Diego Freitas 9fcd2420b9 trajetoria atualizada 2026-09-17 23:06:32 -03:00
18 changed files with 4618 additions and 488 deletions

View File

@ -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;
agitador.Kp = (double)paramKp.Value; if (agitador != null)
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.AtuDuracaoAtuacao = (int)paramDuracaoAtuacao.Value;
parametros.AtuPressaoLinha = (double)paramPressaoLinha.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 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

View File

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

View File

@ -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,18 +155,36 @@ 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)
{ {
FuncoesIHM.MostrarAlerta( int qtdRuas =
"Trajetória não parametrizada", operacaoParametrizando.Parametros?.RuasPercorrer?.Count
$"Para salvar os parâmetros da operação {operacaoParametrizando.Sensoriamento.Operacao.Modo}, é necessário configurar o mapa e as ruas a serem percorridas!", ?? operacaoParametrizando.Mapa?.RuasPercorrer?.Count
MessageBoxIcon.Warning ?? 0;
);
return; 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;
}
} }
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);

View File

@ -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)

View File

@ -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)
{ {

View File

@ -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;
Variaveis.MostrarLog( bool atuSucesso =
"[OperacaoModel.RealizarCalibragemInicialAsync] " + taskCalibragemAtu == null ||
"Tasks de calibragem dos módulos concluídas." (taskCalibragemAtu.Status == TaskStatus.RanToCompletion && taskCalibragemAtu.Result);
);
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem finalizada."); 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."
);
}
} }
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,

View File

@ -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

View File

@ -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);
// Integração // ------------------------------------------------------------
double theta = anguloAtualDeg * Math.PI / 180.0; // 2) Cinemática 4WS no ponto médio entre eixos
double omega = velocidadeMs * kappaEf; // rad/s // ------------------------------------------------------------
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;
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,
Lon0 = GPSService.UltimaLeitura.Lon0, Lat0 = GPSService.UltimaLeitura.Lat0,
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,16 +1300,13 @@ 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;
return novaPosicao; return novaPosicao;
} }

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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():

View File

@ -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():

View File

@ -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.
.Where(x => !string.IsNullOrWhiteSpace(x)) // Exemplo:
.Distinct() // Sen: Desconectado
.ToList() ?? new List<string>(); // 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>();
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?
.Where(x => !string.IsNullOrWhiteSpace(x)) var motivosTrajetoria =
.Distinct() obj.Trajetoria?.AutonomiaCorredor?.Motivos?
.ToList() ?? new List<string>(); .Where(x => !string.IsNullOrWhiteSpace(x))
.Select(NormalizarTexto)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.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á
.Where(mod => // liberada, porém ainda não está executando.
(mod?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false) || //
( // Exemplos:
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(mod?.status ?? StatusModulo.Desconectado) && // Aguardando retomada segura
(rover?.ModulosMandatorios?.Any(x => // Parametrizando
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) && // Finalizando
(x?.Utilizar ?? false) && // ============================================================
(x?.Mandatorio ?? false)
) ?? false) if (
) || obj.Operacao != null &&
( obj.Operacao.Liberada &&
(mod?.status ?? StatusModulo.Desconectado) == StatusModulo.Alerta && obj.Operacao.Pausa != true &&
(rover?.ModulosMandatorios?.Any(x => obj.Operacao.Emergencia != true &&
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) && obj.Operacao.Status != StatusOperacao.EmAndamento &&
(x?.Utilizar ?? false) && !string.IsNullOrWhiteSpace(obj.Operacao.MotivoStatusOperacao)
(x?.Mandatorio ?? false) )
) ?? false) {
) var motivo =
) NormalizarTexto(obj.Operacao.MotivoStatusOperacao);
.ToList() ?? new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>();
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>();
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 =
.Where(cond => cond?.severidade > 90) modulo?.condicoes_operacionais?
.Select(cond => cond?.descricao ?? "") .Where(cond => cond?.severidade > 90)
.Where(desc => !string.IsNullOrWhiteSpace(desc)) .Select(cond =>
.Distinct() NormalizarTexto(cond?.descricao))
.ToList() ?? new List<string>(); .Where(desc =>
!string.IsNullOrWhiteSpace(desc))
.Distinct()
.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,17 +991,38 @@ 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
.Select(g => g.First()) .Where(x =>
.OrderByDescending(x => x.Prioridade) !string.IsNullOrWhiteSpace(x.Titulo) &&
.ThenBy(x => x.Modulo.HasValue && ordemModulos.ContainsKey(x.Modulo.Value)? ordemModulos[x.Modulo.Value] : int.MaxValue) !string.IsNullOrWhiteSpace(x.Descricao))
.ThenBy(x => x.Fonte) .GroupBy(x => new
.ToList(); {
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
// ============================================================
if (!eventosOrdenados.Any()) if (!eventosOrdenados.Any())
{ {
@ -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.",
? StatusModulo.Desconectado StatusVisual =
: StatusModulo.Operante obj.StatusRover == StatusModulo.Desconectado
? StatusModulo.Desconectado
: 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;
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 else
{ {
statusVisual = obj.StatusRover == StatusModulo.Desconectado var descricoes =
? StatusModulo.Desconectado eventosOrdenados
: StatusModulo.Operante; .Select(x => x.Descricao?.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.Take(3)
.ToList();
linha1 = "ROVER OPERANDO NORMALMENTE"; linha2 =
linha2 = "Trajetória, controle e operação em condição normal."; string.Join(" • ", descricoes);
badge = statusVisual.ToString().ToUpper();
resumo = (obj.Operacao?.Status ?? StatusOperacao.NaoIniciado).ToString();
} }
_viewOperacaoCenter?.Historico?._vm?.CompararERegistrarMudancasEventos(rover.RoverId, eventosOrdenados); 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";
// 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 =
_viewOperacaoTop?._vm?.AtualizarStatus( obj.Operacao?.Status.ToString()
statusVisual, ?? StatusOperacao.NaoIniciado.ToString();
rover.RoverId,
obj?.Operacao?.Status ?? StatusOperacao.NaoIniciado, // ============================================================
linha1, // 15) Atualização da UI
linha2, // ============================================================
badge,
resumo 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) public void AtualizarBarraSuperior(StatusModulo status, string titulo, string linha1, string linha2, string status_str, string resumo)