ajuste no stream de video, alinhamento do carro no corredor, manobras, ihm, simulação cinemática,

This commit is contained in:
Diego Freitas 2026-09-17 23:10:59 -03:00
parent 9fcd2420b9
commit b737b11a59
18 changed files with 3708 additions and 478 deletions

View File

@ -1,4 +1,4 @@
using AgroBase.Models;
using AgroBase.Models;
using System;
using System.Windows.Forms;
@ -52,6 +52,8 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
private void CarregarDadosTela()
{
_carregandoDados = true;
try
{
var parametros = op?.Parametros?.Controle;
@ -97,9 +99,12 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
parametros.AtuAgitadorModo = seletorModoAgitador.SelectedMode;
agitador.Kp = (double)paramKp.Value;
agitador.Ki = (double)paramKi.Value;
agitador.Kd = (double)paramKd.Value;
if (agitador != null)
{
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,6 +14,7 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
private bool _carregandoDados;
private bool _mapaExistenteCarregado;
private bool _hidratandoMapaExistente;
public ucParametrosMapa()
{
@ -56,8 +57,12 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
if (mesmaInstanciaMapa && !recarregarMapa)
{
_mapaExistenteCarregado = true;
lblMapaPlaceholder.Visible = _mapa.mapaService?.DadosMapa == null;
SincronizarParametrosComMapa();
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);
AtualizarResumo();
return;
}
@ -72,17 +77,28 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
try
{
lblMapaPlaceholder.Visible = _mapa?.mapaService?.DadosMapa == null;
MapaFeatureCollectionModel mapaRuntime =
_mapa?.mapaService?.DadosMapa;
SincronizarParametrosComMapa();
AtualizarResumo();
lblMapaPlaceholder.Visible =
(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)
{
_mapaExistenteCarregado = true;
AtualizarResumo();
CarregarMapaExistenteAsync();
}
else
{
SincronizarParametrosComMapa(permitirSelecaoVazia: false);
AtualizarResumo();
}
}
finally
{
@ -92,28 +108,57 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
private async void CarregarMapaExistenteAsync()
{
if (_hidratandoMapaExistente)
return;
_hidratandoMapaExistente = true;
try
{
if (_mapa == null || op?.Parametros?.Mapa == null)
{
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;
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;
AtualizarResumo();
}
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)
return;
if (_hidratandoMapaExistente)
return;
if (InvokeRequired)
{
BeginInvoke(new Action(
@ -150,13 +198,13 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
lblMapaPlaceholder.Visible = false;
SincronizarParametrosComMapa();
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
AtualizarResumo();
}
private void Mapa_RuasSelecionadasAlteradas(object sender, EventArgs e)
{
if (_carregandoDados)
if (_carregandoDados || _hidratandoMapaExistente)
return;
if (IsDisposed || Disposing)
@ -174,27 +222,52 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
return;
}
SincronizarParametrosComMapa();
SincronizarParametrosComMapa(permitirSelecaoVazia: true);
AtualizarResumo();
}
private void SincronizarParametrosComMapa()
private void SincronizarParametrosComMapa(bool permitirSelecaoVazia)
{
if (op?.Parametros == null || _mapa == null)
{
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.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()
{
MapaFeatureCollectionModel dadosMapa = _mapa?.mapaService?.DadosMapa;
List<MapaFeatureModel> features = dadosMapa?.features ?? new List<MapaFeatureModel>();
List<string> selecionadas = _mapa?.RuasPercorrer ?? new List<string>();
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>());
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,6 +193,8 @@ namespace AgroBase.Forms.IHM.Operacao
pnlAlertaAutonomia.Visible = false;
pnlStatusAutonomia.Visible = true;
btnLiberarSupervisao.Enabled = false;
pnlStatusAutonomia.BringToFront();
}
@ -204,6 +206,36 @@ 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 =
@ -227,8 +259,44 @@ 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();
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);
}
@ -467,13 +535,21 @@ namespace AgroBase.Forms.IHM.Operacao
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);
MostrarAlertaAutonomia();
// MostrarStatusAutonomia() desabilita o botão, então restauramos
// explicitamente o estado após trazer o painel de alerta.
btnLiberarSupervisao.Enabled = true;
return;
}
@ -483,11 +559,13 @@ 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;
@ -644,10 +722,7 @@ namespace AgroBase.Forms.IHM.Operacao
lblStatusAvaliacao.Text = !string.IsNullOrWhiteSpace(autonomia.Motivo) ? autonomia.Motivo : "Autonomia atualizada";
lblStatusAvaliacao.ForeColor = autonomia.Liberado ? corCorredor : HmiTheme.TextMuted;
if (autonomia.Liberado)
MostrarStatusAutonomia();
else
MostrarAlertaAutonomia();
AtualizarPainelAutonomia(autonomia);
}

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,18 +155,36 @@ namespace AgroBase.Forms.IHM.Operacao
if (operacaoParametrizando == null)
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(
"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;
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;
}
}
var op = Variaveis.OperacaoEmAndamento;
if (op?.Sensoriamento?.Operacao?.OperacaoIniciada ?? false)
if (operacaoIniciada)
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 =
$"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 =
$"Tempo Restante: {dadosTrajetoria.TempoEstimadoRestante}";
lblTempoOperacao.Text =
@ -1446,6 +1446,15 @@ 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,12 +2599,24 @@ namespace AgroBase.Models.Modules
if (op?.DispAtu?.Dados == null)
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;
}
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;
@ -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));
}
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,6 +349,12 @@ 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
@ -361,6 +367,12 @@ 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()
@ -1395,6 +1407,11 @@ 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 }));
@ -1665,6 +1682,12 @@ 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;
@ -1771,9 +1794,16 @@ 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)
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)
tasks.AddRange(dispMvd.Dados.RealizarCalibragemDirAsync(forcar));
@ -1799,12 +1829,56 @@ namespace AgroBase.Models
{
await allTasks;
Variaveis.MostrarLog(
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
"Tasks de calibragem dos módulos concluídas."
);
bool atuSucesso =
taskCalibragemAtu == null ||
(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
{
@ -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
{
_calibragemGate.Release();
@ -3291,10 +3376,9 @@ namespace AgroBase.Models
op.RegistrarTentativaCalibragem();
_ = Task.Run(async () => await op.RealizarCalibragemInicialAsync(false));
}
if (op.CalibrandoRuntime && op.TodosModulosMandatoriosCalibrados())
{
op.DefinirEstadoCalibragemRuntime(false);
}
// O estado Calibrando pertence exclusivamente à rotina de calibração.
// 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"),
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),
@ -5375,6 +5462,8 @@ 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; }
@ -5439,6 +5528,8 @@ namespace AgroBase.Models
Status = Status,
ErroOperacaoLiberada = ErroOperacaoLiberada,
OperacaoLiberacaoDebug = OperacaoLiberacaoDebug,
MotivoStatusOperacao = MotivoStatusOperacao,
UltimoMotivoParada = UltimoMotivoParada,
OperacaoConfigurada = OperacaoConfigurada,
OperacaoLiberada = OperacaoLiberada,
OperacaoIniciada = OperacaoIniciada,

View File

@ -712,6 +712,8 @@ 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,
@ -1497,6 +1499,8 @@ 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; }

View File

@ -114,8 +114,108 @@ namespace AgroBase.Models
private int MaxPontosEntradaRecuperacaoTransicao { get; set; } = 12;
private int ConfirmacoesMinimasEntradaProximoCorredor { get; set; } = 2;
// V11 - watchdog de progresso da trajetória.
//
// Filosofia:
// - NÃO julga saúde por repetição de comando/ângulo. Uma rua reta pode
// operar por muito tempo com ângulo 0 e velocidade constante.
// - julga progresso FÍSICO + GEOMÉTRICO: o rover precisa estar andando
// e, ao mesmo tempo, reduzir a distância restante / distância ao
// próximo ponto ou avançar o prefixo autoritativo.
// - Suspeita é passiva. Reaquisição apenas limita a velocidade e dá
// tempo para as recuperações estritas já existentes agirem.
// - se mesmo assim não houver progresso, a saída é falha fechada:
// velocidade 0 + operação não liberada.
public bool WatchdogProgressoHabilitado { get; set; } = true;
public double WatchdogVelocidadeMinimaMs { get; set; } = 0.12;
public double WatchdogProgressoRestanteMinimoM { get; set; } = 0.22;
public double WatchdogProgressoAlvoMinimoM { get; set; } = 0.15;
public double WatchdogTempoMinimoSuspeitaS { get; set; } = 2.50;
public double WatchdogTempoMinimoReaquisicaoS { get; set; } = 3.75;
public double WatchdogTempoMaximoReaquisicaoS { get; set; } = 4.00;
public double WatchdogDistanciaMaximaReaquisicaoM { get; set; } = 1.50;
public double WatchdogTempoMaximoSemMovimentoS { get; set; } = 6.00;
public double WatchdogDistanciaMinimaMovimentoM { get; set; } = 0.15;
public double WatchdogAnguloMinimoInversaoGraus { get; set; } = 3.0;
public int WatchdogInversoesParaReaquisicao { get; set; } = 4;
#endregion
public enum EstadoWatchdogTrajetoria
{
Desarmado = 0,
Normal = 1,
Suspeita = 2,
Reaquisicao = 3,
ParadaSegura = 4
}
[JsonProperty]
public EstadoWatchdogTrajetoria WatchdogEstado { get; private set; } = EstadoWatchdogTrajetoria.Desarmado;
[JsonProperty]
public string WatchdogMotivo { get; private set; } = string.Empty;
[JsonProperty]
public double WatchdogDistanciaFisicaJanelaM { get; private set; } = 0.0;
[JsonProperty]
public int WatchdogInversoesAnguloJanela { get; private set; } = 0;
[JsonIgnore]
private DateTime _watchdogInicioJanelaUtc = DateTime.MinValue;
[JsonIgnore]
private DateTime _watchdogInicioReaquisicaoUtc = DateTime.MinValue;
[JsonIgnore]
private double _watchdogDistanciaFisicaInicioReaquisicaoM = 0.0;
[JsonIgnore]
private int _watchdogIdxPontoAtualRef = -1;
[JsonIgnore]
private int _watchdogIdxProximoPontoRef = -1;
[JsonIgnore]
private int _watchdogIdxCorredorRef = -1;
[JsonIgnore]
private int _watchdogQtdPontosRef = -1;
[JsonIgnore]
private StatusCarroMapa _watchdogStatusRef = StatusCarroMapa.Parado;
[JsonIgnore]
private double _watchdogDistanciaRestanteRefM = double.NaN;
[JsonIgnore]
private double _watchdogDistanciaProximoRefM = double.NaN;
[JsonIgnore]
private double _watchdogUltimoAnguloComandoGraus = 0.0;
[JsonIgnore]
private bool _watchdogUltimoAnguloValido = false;
[JsonIgnore]
private double _watchdogUltimoTimestampGps = double.NaN;
[JsonIgnore]
private DateTime _watchdogUltimoMomentoGps = DateTime.MinValue;
[JsonIgnore]
private double _watchdogUltimaLatitudeGps = double.NaN;
[JsonIgnore]
private double _watchdogUltimaLongitudeGps = double.NaN;
[JsonIgnore]
private DateTime _watchdogUltimaReducaoVelocidadeUtc = DateTime.MinValue;
[JsonIgnore]
private DateTime _watchdogUltimaReafirmacaoParadaUtc = DateTime.MinValue;
public DateTime UltimaAtualizacaoDados { get; set; } = DateTime.MinValue;
public bool RetornandoBase { get; set; }
public AutonomiaCorredorModel AutonomiaCorredor { get; set; }
@ -861,6 +961,549 @@ namespace AgroBase.Models
var _trajetoria = (_TrajetoriaDinamica ?? new List<PontoTrajetoriaModel>()).Select(x => x.Posicao).ToList();
DistanciaRestante = GPSUtils.DistanciaDoTrecho(_trajetoria);
}
private bool WatchdogNovaAmostraGps()
{
var gps = GPSPosicaoAtual;
if (gps == null)
return false;
double ts = gps.TimestampPos?.valor ?? double.NaN;
if (ValorFinito(ts))
{
if (ValorFinito(_watchdogUltimoTimestampGps) &&
Math.Abs(ts - _watchdogUltimoTimestampGps) <= ToleranciaCoordenada)
{
return false;
}
_watchdogUltimoTimestampGps = ts;
_watchdogUltimoMomentoGps = gps.Momento;
_watchdogUltimaLatitudeGps = gps.Latitude;
_watchdogUltimaLongitudeGps = gps.Longitude;
return true;
}
if (gps.Momento != DateTime.MinValue)
{
if (gps.Momento == _watchdogUltimoMomentoGps)
return false;
_watchdogUltimoMomentoGps = gps.Momento;
_watchdogUltimaLatitudeGps = gps.Latitude;
_watchdogUltimaLongitudeGps = gps.Longitude;
return true;
}
if (ValorFinito(_watchdogUltimaLatitudeGps) &&
ValorFinito(_watchdogUltimaLongitudeGps) &&
Math.Abs(gps.Latitude - _watchdogUltimaLatitudeGps) <= ToleranciaCoordenada &&
Math.Abs(gps.Longitude - _watchdogUltimaLongitudeGps) <= ToleranciaCoordenada)
{
return false;
}
_watchdogUltimaLatitudeGps = gps.Latitude;
_watchdogUltimaLongitudeGps = gps.Longitude;
return true;
}
private double ObterDistanciaMinimaSuspeitaWatchdogM()
{
switch (StatusAtual)
{
case StatusCarroMapa.Manobrando:
case StatusCarroMapa.EntrandoRua:
case StatusCarroMapa.SaindoRua:
return 1.00;
case StatusCarroMapa.CaminhandoRua:
return 1.20;
case StatusCarroMapa.Direcionando:
case StatusCarroMapa.RetornandoBase:
return 1.50;
default:
return 1.50;
}
}
private void ReiniciarJanelaWatchdog(
EstadoWatchdogTrajetoria novoEstado,
string motivo,
bool registrarRecuperacao = false
)
{
var estadoAnterior = WatchdogEstado;
_watchdogInicioJanelaUtc = DateTime.UtcNow;
_watchdogInicioReaquisicaoUtc = DateTime.MinValue;
_watchdogDistanciaFisicaInicioReaquisicaoM = 0.0;
_watchdogIdxPontoAtualRef = PontoAtual?.idxPonto ?? -1;
_watchdogIdxProximoPontoRef = ProximoPonto?.idxPonto ?? -1;
_watchdogIdxCorredorRef = CorredorAtual?.Idx ?? -1;
_watchdogQtdPontosRef = _TrajetoriaFixa?.Count ?? 0;
_watchdogStatusRef = StatusAtual;
_watchdogDistanciaRestanteRefM = DistanciaRestante;
_watchdogDistanciaProximoRefM =
(GPSPosicaoAtual != null && ProximoPonto?.Posicao != null)
? GPSUtils.DistanciaEntrePontos(GPSPosicaoAtual, ProximoPonto.Posicao)
: double.NaN;
WatchdogDistanciaFisicaJanelaM = 0.0;
WatchdogInversoesAnguloJanela = 0;
_watchdogUltimoAnguloComandoGraus = op?.Controle?.Angulo ?? 0.0;
_watchdogUltimoAnguloValido = Math.Abs(_watchdogUltimoAnguloComandoGraus) >= WatchdogAnguloMinimoInversaoGraus;
WatchdogEstado = novoEstado;
WatchdogMotivo = motivo ?? string.Empty;
if (registrarRecuperacao &&
estadoAnterior != EstadoWatchdogTrajetoria.Normal &&
estadoAnterior != EstadoWatchdogTrajetoria.Desarmado)
{
string msg =
$"[TRJ/WATCHDOG] Progresso recuperado. " +
$"estado={estadoAnterior}->Normal, " +
$"idx={_watchdogIdxPontoAtualRef}, " +
$"status={StatusAtual}.";
Variaveis.MostrarLog(msg);
op?.Sensoriamento?.InserirLog(
T_Code.Trj,
StatusModulo.Operante,
100,
msg
);
}
}
private void DesarmarWatchdog(string motivo, bool limparParadaSegura)
{
if (WatchdogEstado == EstadoWatchdogTrajetoria.ParadaSegura &&
!limparParadaSegura)
{
return;
}
ReiniciarJanelaWatchdog(
EstadoWatchdogTrajetoria.Desarmado,
motivo ?? string.Empty,
registrarRecuperacao: false
);
}
private void EntrarSuspeitaWatchdog(string motivo)
{
if (WatchdogEstado == EstadoWatchdogTrajetoria.Suspeita ||
WatchdogEstado == EstadoWatchdogTrajetoria.Reaquisicao ||
WatchdogEstado == EstadoWatchdogTrajetoria.ParadaSegura)
{
return;
}
WatchdogEstado = EstadoWatchdogTrajetoria.Suspeita;
WatchdogMotivo = motivo;
string msg =
$"[TRJ/WATCHDOG] SUSPEITA de falta de progresso. " +
$"idx={PontoAtual?.idxPonto}, prox={ProximoPonto?.idxPonto}, " +
$"status={StatusAtual}, fisico={WatchdogDistanciaFisicaJanelaM:F2}m, " +
$"inversoes={WatchdogInversoesAnguloJanela}, motivo={motivo}";
Variaveis.MostrarLog(msg);
op?.Sensoriamento?.InserirLog(
T_Code.Trj,
StatusModulo.Alerta,
80,
msg
);
}
private void LimitarVelocidadeReaquisicaoWatchdog()
{
if ((DateTime.UtcNow - _watchdogUltimaReducaoVelocidadeUtc).TotalSeconds < 0.50)
return;
var parametrosControle = op?.Parametros?.Controle;
var controle = op?.Controle;
if (parametrosControle == null || controle == null)
return;
double velocidadeSeguraPercent = parametrosControle.MovVelocidadeCErvasPercent;
if (!ValorFinito(velocidadeSeguraPercent) ||
velocidadeSeguraPercent <= 0 ||
controle.PercentualVelocidadeSP <= velocidadeSeguraPercent + 0.1)
{
return;
}
RedisService.AtualizarCampos(
CtxKey.DadosControle,
("velocidade_sp", velocidadeSeguraPercent)
);
_watchdogUltimaReducaoVelocidadeUtc = DateTime.UtcNow;
}
private void EntrarReaquisicaoWatchdog(string motivo)
{
if (WatchdogEstado == EstadoWatchdogTrajetoria.Reaquisicao ||
WatchdogEstado == EstadoWatchdogTrajetoria.ParadaSegura)
{
return;
}
WatchdogEstado = EstadoWatchdogTrajetoria.Reaquisicao;
WatchdogMotivo = motivo;
_watchdogInicioReaquisicaoUtc = DateTime.UtcNow;
_watchdogDistanciaFisicaInicioReaquisicaoM = WatchdogDistanciaFisicaJanelaM;
// Não troca status, não muda geometria e não marca waypoint.
// As recuperações estritas do fluxo normal continuam soberanas.
// A única intervenção é limitar a velocidade ao perfil seguro que
// já existe no equipamento, caso o Python esteja mandando mais.
LimitarVelocidadeReaquisicaoWatchdog();
string msg =
$"[TRJ/WATCHDOG] REAQUISICAO armada. " +
$"idx={PontoAtual?.idxPonto}, prox={ProximoPonto?.idxPonto}, " +
$"status={StatusAtual}, fisico={WatchdogDistanciaFisicaJanelaM:F2}m, " +
$"inversoes={WatchdogInversoesAnguloJanela}, motivo={motivo}";
Variaveis.MostrarLog(msg);
op?.Sensoriamento?.InserirLog(
T_Code.Trj,
StatusModulo.Alerta,
70,
msg
);
}
private void AcionarParadaSeguraWatchdog(string motivo)
{
if (WatchdogEstado == EstadoWatchdogTrajetoria.ParadaSegura)
return;
WatchdogEstado = EstadoWatchdogTrajetoria.ParadaSegura;
WatchdogMotivo = motivo;
RedisService.AtualizarCampos(
CtxKey.DadosControle,
("velocidade_sp", 0)
);
RedisService.AtualizarCampos(
CtxKey.DadosOperacao,
("liberado", false),
("motivo_nao_liberado", "Watchdog de trajetória: " + motivo)
);
_watchdogUltimaReafirmacaoParadaUtc = DateTime.UtcNow;
string msg =
$"[TRJ/WATCHDOG] PARADA SEGURA. " +
$"idx={PontoAtual?.idxPonto}, prox={ProximoPonto?.idxPonto}, " +
$"status={StatusAtual}, fisico={WatchdogDistanciaFisicaJanelaM:F2}m, " +
$"inversoes={WatchdogInversoesAnguloJanela}, motivo={motivo}";
Variaveis.MostrarLog(msg);
op?.Sensoriamento?.InserirLog(
T_Code.Trj,
StatusModulo.Falha,
0,
msg
);
}
private void ReafirmarParadaSeguraWatchdog()
{
if ((DateTime.UtcNow - _watchdogUltimaReafirmacaoParadaUtc).TotalSeconds < 0.50)
return;
RedisService.AtualizarCampos(
CtxKey.DadosControle,
("velocidade_sp", 0)
);
_watchdogUltimaReafirmacaoParadaUtc = DateTime.UtcNow;
}
private void AtualizarWatchdogProgressoTrajetoria()
{
bool operacaoEmAndamento =
op?.Sensoriamento?.Operacao?.StatusOperacaoAtual ==
StatusOperacao.EmAndamento;
bool controleAutomatico = op?.Parametros?.ControleAutomatico ?? false;
bool controleManualAcionado = op?.Parametros?.Controle?.ControleManualAcionado == true;
// Fora da operação automática, o operador tem autoridade para
// reposicionar/rearmar o equipamento. Uma nova entrada no modo
// automático começa com janela limpa.
if (!WatchdogProgressoHabilitado ||
!operacaoEmAndamento ||
!controleAutomatico ||
controleManualAcionado)
{
DesarmarWatchdog(
"watchdog fora do regime automático",
limparParadaSegura: !operacaoEmAndamento || controleManualAcionado
);
return;
}
if (WatchdogEstado == EstadoWatchdogTrajetoria.ParadaSegura)
{
ReafirmarParadaSeguraWatchdog();
return;
}
if (!_TrajetoriaFixaDefinida ||
GPSPosicaoAtual == null ||
GPSPosicaoAnterior == null ||
PontoAtual == null ||
ProximoPonto == null ||
ReferenceEquals(PontoAtual, ProximoPonto) ||
TrajeotiraConcluida ||
StatusAtual == StatusCarroMapa.Parado)
{
DesarmarWatchdog("trajetória sem condição de monitoramento", limparParadaSegura: false);
return;
}
var controle = op?.Controle;
if (controle == null || controle.EmFreio)
{
DesarmarWatchdog("movimento freado", limparParadaSegura: false);
return;
}
double velocidadeComandadaMs =
FuncoesMatematicas.CalculaVelocidadeMsPercentual(
controle.PercentualVelocidadeSP
);
if (!ValorFinito(velocidadeComandadaMs) ||
velocidadeComandadaMs < WatchdogVelocidadeMinimaMs)
{
DesarmarWatchdog("velocidade abaixo do limiar do watchdog", limparParadaSegura: false);
return;
}
if (!WatchdogNovaAmostraGps())
return;
DateTime agoraUtc = DateTime.UtcNow;
int idxAtual = PontoAtual.idxPonto;
int idxProximo = ProximoPonto.idxPonto;
int idxCorredor = CorredorAtual?.Idx ?? PontoAtual.idxCorredor;
int qtdPontos = _TrajetoriaFixa?.Count ?? 0;
double distanciaProximo =
GPSUtils.DistanciaEntrePontos(
GPSPosicaoAtual,
ProximoPonto.Posicao
);
bool referenciaMudou =
_watchdogInicioJanelaUtc == DateTime.MinValue ||
_watchdogIdxPontoAtualRef != idxAtual ||
_watchdogIdxProximoPontoRef != idxProximo ||
_watchdogIdxCorredorRef != idxCorredor ||
_watchdogQtdPontosRef != qtdPontos ||
_watchdogStatusRef != StatusAtual;
if (referenciaMudou)
{
ReiniciarJanelaWatchdog(
EstadoWatchdogTrajetoria.Normal,
"referência autoritativa avançou ou mudou",
registrarRecuperacao: true
);
return;
}
double passoFisico =
GPSUtils.DistanciaEntrePontos(
GPSPosicaoAnterior,
GPSPosicaoAtual
);
double distanciaEsperadaAmostra =
velocidadeComandadaMs /
Math.Max(1, GPSService.TaxaAmostragemHz);
double limiteSaltoGps =
Math.Max(
0.75,
distanciaEsperadaAmostra * 6.0 + 0.35
);
if (!ValorFinito(passoFisico) || passoFisico < 0)
passoFisico = 0;
if (passoFisico > limiteSaltoGps)
{
// Um salto GNSS não pode ser interpretado nem como progresso
// nem como ausência de progresso. Reinicia a janela e espera
// novas amostras coerentes.
ReiniciarJanelaWatchdog(
EstadoWatchdogTrajetoria.Normal,
$"salto GNSS ignorado ({passoFisico:F2}m)",
registrarRecuperacao: false
);
return;
}
double pisoMovimentoGps =
Math.Max(0.015, distanciaEsperadaAmostra * 0.15);
if (passoFisico >= pisoMovimentoGps)
WatchdogDistanciaFisicaJanelaM += passoFisico;
double anguloAtual = controle.Angulo;
bool anguloAtualValido =
ValorFinito(anguloAtual) &&
Math.Abs(anguloAtual) >= WatchdogAnguloMinimoInversaoGraus;
if (_watchdogUltimoAnguloValido &&
anguloAtualValido &&
Math.Sign(_watchdogUltimoAnguloComandoGraus) != Math.Sign(anguloAtual))
{
WatchdogInversoesAnguloJanela++;
}
if (anguloAtualValido)
{
_watchdogUltimoAnguloComandoGraus = anguloAtual;
_watchdogUltimoAnguloValido = true;
}
double melhoraRestante =
ValorFinito(_watchdogDistanciaRestanteRefM) && ValorFinito(DistanciaRestante)
? _watchdogDistanciaRestanteRefM - DistanciaRestante
: 0.0;
double melhoraProximo =
ValorFinito(_watchdogDistanciaProximoRefM) && ValorFinito(distanciaProximo)
? _watchdogDistanciaProximoRefM - distanciaProximo
: 0.0;
bool progressoGeometrico =
melhoraRestante >= WatchdogProgressoRestanteMinimoM ||
melhoraProximo >= WatchdogProgressoAlvoMinimoM;
if (progressoGeometrico)
{
ReiniciarJanelaWatchdog(
EstadoWatchdogTrajetoria.Normal,
$"progresso geométrico confirmado (rest={melhoraRestante:F2}m, alvo={melhoraProximo:F2}m)",
registrarRecuperacao: true
);
return;
}
double tempoJanelaS =
Math.Max(
0.0,
(agoraUtc - _watchdogInicioJanelaUtc).TotalSeconds
);
// Caso separado: existe comando de movimento há bastante tempo,
// mas o rover praticamente não saiu do lugar. Isso é mais parecido
// com obstáculo/falha mecânica do que com erro de waypoint.
if (tempoJanelaS >= WatchdogTempoMaximoSemMovimentoS &&
WatchdogDistanciaFisicaJanelaM < WatchdogDistanciaMinimaMovimentoM)
{
AcionarParadaSeguraWatchdog(
$"movimento comandado por {tempoJanelaS:F1}s, mas deslocamento físico foi apenas " +
$"{WatchdogDistanciaFisicaJanelaM:F2}m"
);
return;
}
double distanciaMinimaSuspeita =
ObterDistanciaMinimaSuspeitaWatchdogM();
if (WatchdogEstado == EstadoWatchdogTrajetoria.Normal &&
tempoJanelaS >= WatchdogTempoMinimoSuspeitaS &&
WatchdogDistanciaFisicaJanelaM >= distanciaMinimaSuspeita)
{
EntrarSuspeitaWatchdog(
$"percorreu {WatchdogDistanciaFisicaJanelaM:F2}m sem reduzir a rota restante nem o próximo alvo"
);
}
bool muitasInversoes =
WatchdogInversoesAnguloJanela >=
WatchdogInversoesParaReaquisicao;
double distanciaMinimaReaquisicao =
distanciaMinimaSuspeita * 1.55;
bool evidenciaForteReaquisicao =
tempoJanelaS >= WatchdogTempoMinimoReaquisicaoS &&
(
WatchdogDistanciaFisicaJanelaM >= distanciaMinimaReaquisicao ||
(
muitasInversoes &&
WatchdogDistanciaFisicaJanelaM >= distanciaMinimaSuspeita * 1.20
)
);
if (WatchdogEstado == EstadoWatchdogTrajetoria.Suspeita &&
evidenciaForteReaquisicao)
{
EntrarReaquisicaoWatchdog(
muitasInversoes
? "falta de progresso + inversões repetidas do comando direcional"
: "falta de progresso persistente apesar de deslocamento físico"
);
}
if (WatchdogEstado != EstadoWatchdogTrajetoria.Reaquisicao)
return;
LimitarVelocidadeReaquisicaoWatchdog();
double tempoReaquisicaoS =
Math.Max(
0.0,
(agoraUtc - _watchdogInicioReaquisicaoUtc).TotalSeconds
);
double distanciaFisicaReaquisicaoM =
Math.Max(
0.0,
WatchdogDistanciaFisicaJanelaM -
_watchdogDistanciaFisicaInicioReaquisicaoM
);
bool excedeuDistanciaReaquisicao =
distanciaFisicaReaquisicaoM >=
WatchdogDistanciaMaximaReaquisicaoM;
bool excedeuTempoReaquisicao =
tempoReaquisicaoS >= WatchdogTempoMaximoReaquisicaoS &&
distanciaFisicaReaquisicaoM >= 0.40;
if (excedeuDistanciaReaquisicao || excedeuTempoReaquisicao)
{
AcionarParadaSeguraWatchdog(
$"reaquisição não convergiu: tempo={tempoReaquisicaoS:F1}s, " +
$"deslocamento={distanciaFisicaReaquisicaoM:F2}m"
);
}
}
[JsonProperty]
public double PercentualTrajetoria { get; private set; }
public void AtualizarPercentualTrajetoria()
@ -1482,6 +2125,12 @@ namespace AgroBase.Models
AtualizarTrajetoriaDinamicaCore();
AtualizarDistanciaRestante();
// V11 - supervisão de progresso. Executa somente depois de termos
// posição, status, trajetória dinâmica e distância restante todos
// atualizados no mesmo snapshot GNSS do ciclo.
AtualizarWatchdogProgressoTrajetoria();
AtualizarPercentualTrajetoria();
AtualizarTempoEstimado();
AtualizarTrajetoriaConcluida();

View File

@ -1084,87 +1084,215 @@ namespace AgroBase.Models
return novoPonto;
}
public static GPSModel SimularNovaPosicao(TipoMovimentoDirecional tipoMovimento, double anguloControleDeg, double velocidadeMs, double tempoDelta, double anguloAtualDeg, GPSModel pos)
{
double L = DistanciaEntreEixosCm / 100.0;
// 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)
{
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)
{
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;
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;
}
// 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 df = dfDeg * Math.PI / 180.0;
double dr = drDeg * Math.PI / 180.0;
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;
double tf = Math.Tan(df);
double tr = Math.Tan(dr);
// Correção dinâmica (opcional)
double Ku = 2.5; // ajuste fino em campo
double kappaEf = kappa / (1 + Ku * velocidadeMs * velocidadeMs);
if (Math.Abs(tf) < 1e-12) tf = 0.0;
if (Math.Abs(tr) < 1e-12) tr = 0.0;
// Integração
double theta = anguloAtualDeg * Math.PI / 180.0;
double omega = velocidadeMs * kappaEf; // rad/s
// ------------------------------------------------------------
// 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;
double dtheta = omega * tempoDelta;
theta += dtheta;
double theta1 = theta0 + dtheta;
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 phi0 = theta0 + beta;
// >>> mesma convenção do Python: 0=Norte
double dx = velocidadeMs * tempoDelta * Math.Sin(heading); // Leste(+)
double dy = velocidadeMs * tempoDelta * Math.Cos(heading); // Norte(+)
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));
}
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 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 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(theta * 180.0 / Math.PI);
double orientacaoMovimentoDeg = GPSUtils.NormalizarAngulo(heading * 180.0 / Math.PI);
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
);
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,
@ -1172,16 +1300,13 @@ 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,10 +576,16 @@ class CameraMultispectral:
)
try:
if stream is not None and hasattr(stream, "parar"):
stream.parar()
except Exception:
pass
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}"
)
with self._lock:
self.ultimo_tensor_multispec = None

View File

@ -223,6 +223,19 @@ 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)
@ -282,10 +295,6 @@ 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,6 +208,8 @@ 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):
@ -1633,6 +1635,15 @@ 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
@ -2343,6 +2354,7 @@ 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):
@ -2417,6 +2429,11 @@ 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
@ -2432,6 +2449,25 @@ 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
@ -2503,6 +2539,10 @@ 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()
@ -2550,6 +2590,20 @@ 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")),
@ -2559,5 +2613,8 @@ 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,6 +1572,10 @@ 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,14 +1651,25 @@ 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:
if self.camera is None:
time.sleep(0.5)
camera_atual = self.camera
if camera_atual is None:
time.sleep(0.25)
continue
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)
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():
self._solicitar_preview(TipoFrameCamera.Debug)
@ -1692,15 +1703,14 @@ 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))
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()
threading.Thread(
target=loop,
daemon=True,
name="visual-stream-feed",
).start()
def _iniciar_loop_analise_continua(self, freq=8.0):
def loop():

View File

@ -3231,14 +3231,26 @@ 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:
if self.camera is None:
time.sleep(5.0)
camera_atual = self.camera
if camera_atual is None:
time.sleep(0.25)
continue
camera_ctx = ContextoGlobalRedis.get_camera(self.mx_id) or {}
@ -3248,9 +3260,11 @@ class CameraManager:
frame_type = TipoFrameCamera(
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)
self.camera.enviar_frame_tcp(frame)
if frame is not None:
camera_atual.enviar_frame_tcp(frame)
if self.debug_visual:
dbg = self._get_cached_preview_frame_ref(TipoFrameCamera.Debug)
@ -3270,15 +3284,14 @@ class CameraManager:
)
lat = time.time() - t0
hz = producer_hz if stream_on else 2.0
time.sleep(max(0.0, (1.0 / hz) - lat))
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()
threading.Thread(
target=loop,
daemon=True,
name="weed-stream-feed",
).start()
def _iniciar_loop_analise_continua(self, freq=15.0):
def loop():

View File

@ -537,6 +537,7 @@ namespace OperationControl.ViewModels
public void AtualizarBarraSuperior(OperacaoParametrosModel rover)
{
var obj = rover?.DadosLeitura;
if (rover == null || obj == null)
return;
@ -545,12 +546,76 @@ namespace OperationControl.ViewModels
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)
{
eventos.Add(new MotivoTopBarModel
@ -558,63 +623,116 @@ 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
});
}
// ============================
// 0) NCP desconectado
// ============================
if (obj.ModulosSaude?.All(x => (x?.status ?? StatusModulo.Desconectado) == StatusModulo.Desconectado) ?? false)
// ============================================================
// 1) NPC / núcleo central
// ============================================================
bool todosModulosDesconectados =
obj.ModulosSaude?.Any() == true &&
obj.ModulosSaude.All(x =>
(x?.status ?? StatusModulo.Desconectado) ==
StatusModulo.Desconectado);
if (todosModulosDesconectados)
{
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
});
}
// ============================
// 1) Emergência
// ============================
// ============================================================
// 2) 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 = "Parada de emergência solicitada.",
Descricao = motivo,
StatusVisual = StatusModulo.Falha
});
}
// ============================
// 2) Pausa
// ============================
// ============================================================
// 3) 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 = "Pausa operacional solicitada.",
Descricao = motivo,
StatusVisual = StatusModulo.Alerta
});
}
// ============================
// 3) Controle
// ============================
var motivosControle = obj.Controle?.Motivos?
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList() ?? new List<string>();
// ============================================================
// 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>();
foreach (var motivo in motivosControle)
{
@ -628,13 +746,18 @@ namespace OperationControl.ViewModels
});
}
// ============================
// 4) Trajetória / autonomia
// ============================
var motivosTrajetoria = obj.Trajetoria?.AutonomiaCorredor?.Motivos?
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList() ?? new List<string>();
// ============================================================
// 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>();
foreach (var motivo in motivosTrajetoria)
{
@ -648,110 +771,215 @@ namespace OperationControl.ViewModels
});
}
// ============================
// 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>();
// ============================================================
// 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>();
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 =
!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 =
(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);
mandatorio &&
status != StatusModulo.Operante &&
status != StatusModulo.Alerta;
bool moduloMandatorioAlerta =
mandatorio &&
status == StatusModulo.Alerta;
// --------------------------------------------------------
// 8.1) Mandatório não operacional
// --------------------------------------------------------
// Caso 1: módulo mandatório não operante
if (moduloMandatorioNaoOperante)
{
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
string motivos = string.Join("; ", modulo?.motivos ?? new List<string>() { "Desconhecido" });
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.";
eventos.Add(new MotivoTopBarModel
{
Prioridade = 620,
Fonte = "ModuloCritico",
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,
Modulo = modulo?.modulo
Modulo = codigoModulo
});
}
// Caso 2: condições operacionais severas
// --------------------------------------------------------
// 8.2) Condições operacionais severas
// --------------------------------------------------------
if (temCondicaoSevera)
{
var descricoesCriticas = modulo?.condicoes_operacionais?
.Where(cond => cond?.severidade > 90)
.Select(cond => cond?.descricao ?? "")
.Where(desc => !string.IsNullOrWhiteSpace(desc))
.Distinct()
.ToList() ?? new List<string>();
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>();
foreach (var desc in descricoesCriticas)
foreach (var descricao in descricoesCriticas)
{
eventos.Add(new MotivoTopBarModel
{
Prioridade = 610,
Fonte = "ModuloCritico",
Titulo = "CONDIÇÕES OPERACIONAIS CRÍTICAS",
Descricao = $"{nomeModulo}: {desc}",
Descricao =
$"{nomeModulo}: {descricao}",
StatusVisual = StatusModulo.Alerta,
Modulo = modulo?.modulo
Modulo = codigoModulo
});
}
}
// Caso 3: módulo mandatório em alerta
// --------------------------------------------------------
// 8.3) Mandatório em alerta
// --------------------------------------------------------
if (moduloMandatorioAlerta)
{
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
string motivos = string.Join("; ", modulo?.motivos ?? new List<string>() { "Desconhecido" });
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.";
eventos.Add(new MotivoTopBarModel
{
Prioridade = 600,
Fonte = "ModuloCritico",
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,
Modulo = modulo?.modulo
Modulo = codigoModulo
});
}
}
// ============================================================
// 9) Ordenação dos módulos
// ============================================================
var ordemModulos = new Dictionary<T_Code, int>
{
{ T_Code.Ipb, 1 },
@ -763,17 +991,38 @@ namespace OperationControl.ViewModels
{ T_Code.Sen, 7 }
};
// ============================
// 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();
// ============================================================
// 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
// ============================================================
if (!eventosOrdenados.Any())
{
@ -782,96 +1031,163 @@ 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
});
}
// ============================
// 7) Monta visual final
// ============================
StatusModulo statusVisual;
string linha1;
// ============================================================
// 12) Montagem visual
// ============================================================
var principal =
eventosOrdenados.First();
StatusModulo statusVisual =
principal.StatusVisual;
string linha1 =
principal.Titulo;
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();
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()
};
linha2 = principal.Descricao;
}
else
{
statusVisual = obj.StatusRover == StatusModulo.Desconectado
? StatusModulo.Desconectado
: StatusModulo.Operante;
var descricoes =
eventosOrdenados
.Select(x => x.Descricao?.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.Take(3)
.ToList();
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();
linha2 =
string.Join(" • ", descricoes);
}
_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))
linha2 = "Sem detalhes adicionais.";
// ============================
// 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
);
}));
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
);
})
);
}
public void AtualizarBarraSuperior(StatusModulo status, string titulo, string linha1, string linha2, string status_str, string resumo)