ajustes gerais pre BP, regras de imu, gps, camera de ervas, status da operacao, fim da calibragem, carregamento do mapa, contador de carga de bateria
This commit is contained in:
parent
69e87c3f20
commit
b148d987b7
|
|
@ -896,6 +896,14 @@ namespace AgroBase
|
||||||
Variaveis.MostrarLog($"[GeneralJoystick] Comando manual não aplicado. Motivo: {motivo}");
|
Variaveis.MostrarLog($"[GeneralJoystick] Comando manual não aplicado. Motivo: {motivo}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void RemoverMotivoParada(string motivo)
|
||||||
|
{
|
||||||
|
var motivos = Variaveis.OperacaoEmAndamento?.Controle?.MotivosManual;
|
||||||
|
if (motivos == null) return;
|
||||||
|
motivos.Remove(motivo);
|
||||||
|
Variaveis.MostrarLog($"[GeneralJoystick] Comando manual não aplicado. Motivo: {motivo}");
|
||||||
|
}
|
||||||
|
|
||||||
public static void EnviarComandoAtuadorAtualizado(DeParaComandos Comando, bool ForcarComando = false)
|
public static void EnviarComandoAtuadorAtualizado(DeParaComandos Comando, bool ForcarComando = false)
|
||||||
{
|
{
|
||||||
var op = Variaveis.OperacaoEmAndamento;
|
var op = Variaveis.OperacaoEmAndamento;
|
||||||
|
|
|
||||||
|
|
@ -454,8 +454,8 @@ namespace AgroBase.Models
|
||||||
MapaFeatureModel feature = mapaService.DadosMapa.features
|
MapaFeatureModel feature = mapaService.DadosMapa.features
|
||||||
.FirstOrDefault(x =>
|
.FirstOrDefault(x =>
|
||||||
x != null &&
|
x != null &&
|
||||||
x.geometry != null &&
|
x.properties != null &&
|
||||||
string.Equals(x.geometry.id, idSelecionado, StringComparison.Ordinal)
|
string.Equals(x.properties?.Id?.Trim(), idSelecionado?.Trim(), StringComparison.Ordinal)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (feature?.geometry?.coordinates == null || feature.geometry.coordinates.Count < 2)
|
if (feature?.geometry?.coordinates == null || feature.geometry.coordinates.Count < 2)
|
||||||
|
|
|
||||||
|
|
@ -1363,10 +1363,15 @@ namespace AgroBase.Models
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
|
|
||||||
op.Sensoriamento.Operacao.OperacaoIniciada = false;
|
op.Sensoriamento.Operacao.OperacaoIniciada = false;
|
||||||
|
op.DefinirEstadoCalibragemRuntime(false);
|
||||||
op.DefinirEmergencia(false);
|
op.DefinirEmergencia(false);
|
||||||
op.Sensoriamento.Operacao.Calibrando = false;
|
|
||||||
op.DefinirPausa(false);
|
op.DefinirPausa(false);
|
||||||
RedisService.AtualizarCampos(CtxKey.DadosOperacao, ("calibrando", false));
|
|
||||||
|
RedisService.AtualizarCampos(
|
||||||
|
CtxKey.DadosOperacao,
|
||||||
|
("calibrando", false),
|
||||||
|
("operacao_iniciada", false)
|
||||||
|
);
|
||||||
|
|
||||||
op.Sensoriamento.Operacao.DataFim = DateTime.Now;
|
op.Sensoriamento.Operacao.DataFim = DateTime.Now;
|
||||||
|
|
||||||
|
|
@ -1732,7 +1737,10 @@ namespace AgroBase.Models
|
||||||
{
|
{
|
||||||
await allTasks;
|
await allTasks;
|
||||||
|
|
||||||
Variaveis.MostrarLog("[OperacaoModel] [RealizarCalibragemInicialAsync] Calibragem finalizada.");
|
Variaveis.MostrarLog(
|
||||||
|
"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||||
|
"Tasks de calibragem dos módulos concluídas."
|
||||||
|
);
|
||||||
|
|
||||||
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem finalizada.");
|
op.Sensoriamento.InserirLog(T_Code.Mod, StatusModulo.Operante, 100, "Calibragem finalizada.");
|
||||||
}
|
}
|
||||||
|
|
@ -1751,11 +1759,73 @@ namespace AgroBase.Models
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
op.DefinirTipoMovimentoControle(tipoControle);
|
try
|
||||||
|
{
|
||||||
|
op.DefinirEstadoCalibragemRuntime(false);
|
||||||
|
|
||||||
op.DefinirEstadoCalibragemRuntime(false);
|
Variaveis.MostrarLog(
|
||||||
|
$"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||||
|
$"Estado global liberado. " +
|
||||||
|
$"Calibrando={op.CalibrandoRuntime}, " +
|
||||||
|
$"Pausa={op.Sensoriamento?.Operacao?.Pausa}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Variaveis.MostrarLog(
|
||||||
|
$"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||||
|
$"Erro ao liberar estado global da calibragem: {ex}"
|
||||||
|
);
|
||||||
|
|
||||||
_calibragemGate.Release();
|
// Fallback direto para não deixar o rover preso em calibragem.
|
||||||
|
if (op.Sensoriamento?.Operacao != null)
|
||||||
|
{
|
||||||
|
op.Sensoriamento.Operacao.Calibrando = false;
|
||||||
|
op.Sensoriamento.Operacao.Pausa = _pausaAntesCalibragem;
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisService.AtualizarCampos(
|
||||||
|
CtxKey.DadosOperacao,
|
||||||
|
("calibrando", false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
op.DefinirTipoMovimentoControle(tipoControle);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Variaveis.MostrarLog(
|
||||||
|
$"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||||
|
$"Erro ao restaurar tipo de movimento: {ex}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Usar aqui o método real existente no GeneralJoystick.
|
||||||
|
GeneralJoystick.RemoverMotivoParada("Calibragem iniciada");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Variaveis.MostrarLog(
|
||||||
|
$"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||||
|
$"Erro ao remover bloqueio manual da calibragem: {ex}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_calibragemGate.Release();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Variaveis.MostrarLog(
|
||||||
|
$"[OperacaoModel.RealizarCalibragemInicialAsync] " +
|
||||||
|
$"Erro ao liberar gate: {ex}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,12 @@ namespace AgroBase.Services
|
||||||
private const double DiferencaCorrenteMinAlerta_A = 3.0;
|
private const double DiferencaCorrenteMinAlerta_A = 3.0;
|
||||||
private const double DiferencaCorrenteRelativaAlerta = 0.35;
|
private const double DiferencaCorrenteRelativaAlerta = 0.35;
|
||||||
|
|
||||||
private const double DtMaximoIntegracao_s = 5.0;
|
/*
|
||||||
|
* O Daly observado em campo pode permanecer 2-3 s com o mesmo
|
||||||
|
* timestamp. A integração só acontece quando chega uma amostra nova.
|
||||||
|
* O limite de 10 s tolera jitter real sem integrar lacunas longas.
|
||||||
|
*/
|
||||||
|
private const double DtMaximoIntegracao_s = 10.0;
|
||||||
|
|
||||||
private readonly double _timeoutBms_s;
|
private readonly double _timeoutBms_s;
|
||||||
private readonly double _timeoutSensor_s;
|
private readonly double _timeoutSensor_s;
|
||||||
|
|
@ -81,8 +86,8 @@ namespace AgroBase.Services
|
||||||
double tensao_nominal_V = 36.0,
|
double tensao_nominal_V = 36.0,
|
||||||
double daly_corrente_descarga_sinal = -1.0,
|
double daly_corrente_descarga_sinal = -1.0,
|
||||||
double sensor_corrente_descarga_sinal = 1.0,
|
double sensor_corrente_descarga_sinal = 1.0,
|
||||||
double timeout_bms_s = 3.0,
|
double timeout_bms_s = 6.0,
|
||||||
double timeout_sensor_s = 3.0)
|
double timeout_sensor_s = 4.0)
|
||||||
{
|
{
|
||||||
CapacidadeMaxima_Ah = Math.Max(0, max_Ah);
|
CapacidadeMaxima_Ah = Math.Max(0, max_Ah);
|
||||||
CargaMaxima_C = CapacidadeMaxima_Ah * 3600.0;
|
CargaMaxima_C = CapacidadeMaxima_Ah * 3600.0;
|
||||||
|
|
@ -140,6 +145,9 @@ namespace AgroBase.Services
|
||||||
private string _ultimaFonteIntegrada = "";
|
private string _ultimaFonteIntegrada = "";
|
||||||
private string _ultimaFonteLogada = "";
|
private string _ultimaFonteLogada = "";
|
||||||
|
|
||||||
|
private double _tensaoAnteriorIntegracao_V;
|
||||||
|
private double _correnteAnteriorIntegracao_A;
|
||||||
|
|
||||||
public double CapacidadeMaxima_Ah { get; private set; }
|
public double CapacidadeMaxima_Ah { get; private set; }
|
||||||
private double CargaMaxima_C { get; set; }
|
private double CargaMaxima_C { get; set; }
|
||||||
|
|
||||||
|
|
@ -507,6 +515,18 @@ namespace AgroBase.Services
|
||||||
private double _somaEnergiaJanela_Wh;
|
private double _somaEnergiaJanela_Wh;
|
||||||
|
|
||||||
private double _distanciaTotalMetrosAnterior;
|
private double _distanciaTotalMetrosAnterior;
|
||||||
|
private double _distanciaPendenteAutonomia_m;
|
||||||
|
private bool _intervaloAutonomiaConfiavel;
|
||||||
|
private bool _aguardandoBaselineAutonomia;
|
||||||
|
|
||||||
|
/* Diagnóstico útil para confirmar o comportamento no próximo campo. */
|
||||||
|
public double DistanciaPendenteAutonomia_m =>
|
||||||
|
_distanciaPendenteAutonomia_m;
|
||||||
|
|
||||||
|
public double DistanciaDescartadaAutonomia_m { get; private set; }
|
||||||
|
|
||||||
|
public string EstadoIntervaloAutonomia { get; private set; } =
|
||||||
|
"aguardando_baseline";
|
||||||
|
|
||||||
public double WhPorMetroJanela { get; private set; }
|
public double WhPorMetroJanela { get; private set; }
|
||||||
|
|
||||||
|
|
@ -607,8 +627,14 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
AplicarFonteAtiva(ativa);
|
AplicarFonteAtiva(ativa);
|
||||||
|
|
||||||
|
if (_estadoInicializado)
|
||||||
|
{
|
||||||
|
AcumularDistanciaPendente(distanciaTotalMetrosAtual);
|
||||||
|
}
|
||||||
|
|
||||||
if (!Iniciado || ativa == null)
|
if (!Iniciado || ativa == null)
|
||||||
{
|
{
|
||||||
|
InvalidarIntervaloAutonomia("fonte_indisponivel");
|
||||||
MarcarAutonomiaIndisponivel();
|
MarcarAutonomiaIndisponivel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -617,6 +643,7 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
if (!leituraAtual.HasValue || leituraAtual.Value == DateTime.MinValue)
|
if (!leituraAtual.HasValue || leituraAtual.Value == DateTime.MinValue)
|
||||||
{
|
{
|
||||||
|
InvalidarIntervaloAutonomia("timestamp_invalido");
|
||||||
MarcarAutonomiaIndisponivel();
|
MarcarAutonomiaIndisponivel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -637,16 +664,13 @@ namespace AgroBase.Services
|
||||||
* Ao trocar de Daly para AB36V, ou vice-versa, não integra
|
* Ao trocar de Daly para AB36V, ou vice-versa, não integra
|
||||||
* o intervalo entre relógios/fontes diferentes.
|
* o intervalo entre relógios/fontes diferentes.
|
||||||
*/
|
*/
|
||||||
if (!string.Equals(_ultimaFonteIntegrada,FonteAtiva, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(_ultimaFonteIntegrada, FonteAtiva, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_ultimaFonteIntegrada = FonteAtiva;
|
DescartarIntervaloPendente("troca_fonte");
|
||||||
|
PrepararBaselineEletrico(ativa, leituraAtual.Value);
|
||||||
_ultimaLeituraIntegrada = leituraAtual.Value;
|
|
||||||
|
|
||||||
_dt = 0;
|
|
||||||
|
|
||||||
AtualizarEstadoEnergia();
|
AtualizarEstadoEnergia();
|
||||||
AtualizarAutonomiaSemIntegrar(distanciaTotalMetrosAtual);
|
RecalcularAutonomia();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -656,26 +680,32 @@ namespace AgroBase.Services
|
||||||
_dt = 0;
|
_dt = 0;
|
||||||
|
|
||||||
AtualizarEstadoEnergia();
|
AtualizarEstadoEnergia();
|
||||||
AtualizarAutonomiaSemIntegrar(distanciaTotalMetrosAtual);
|
RecalcularAutonomia();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_dt = (leituraAtual.Value - _ultimaLeituraIntegrada.Value).TotalSeconds;
|
_dt = (leituraAtual.Value - _ultimaLeituraIntegrada.Value).TotalSeconds;
|
||||||
|
|
||||||
_ultimaLeituraIntegrada = leituraAtual.Value;
|
|
||||||
|
|
||||||
if (_dt <= 0 || _dt > DtMaximoIntegracao_s)
|
if (_dt <= 0 || _dt > DtMaximoIntegracao_s)
|
||||||
{
|
{
|
||||||
Variaveis.MostrarLog($"[COULOMB COUNTER] dt inválido: " + $"{_dt:F3}s | fonte={FonteAtiva}");
|
Variaveis.MostrarLog($"[COULOMB COUNTER] dt inválido: " + $"{_dt:F3}s | fonte={FonteAtiva}");
|
||||||
|
|
||||||
|
DescartarIntervaloPendente("dt_invalido");
|
||||||
|
PrepararBaselineEletrico(ativa, leituraAtual.Value);
|
||||||
|
|
||||||
AtualizarEstadoEnergia();
|
AtualizarEstadoEnergia();
|
||||||
AtualizarAutonomiaSemIntegrar(distanciaTotalMetrosAtual);
|
RecalcularAutonomia();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IntegrarEnergiaECarga(_dt);
|
double deltaEnergiaDescarga_Wh =
|
||||||
|
IntegrarEnergiaECarga(_dt, ativa);
|
||||||
|
|
||||||
|
_ultimaLeituraIntegrada = leituraAtual.Value;
|
||||||
|
_tensaoAnteriorIntegracao_V = ativa.Tensao_V;
|
||||||
|
_correnteAnteriorIntegracao_A = ativa.CorrenteLiquida_A;
|
||||||
|
|
||||||
AtualizarEstadoEnergia();
|
AtualizarEstadoEnergia();
|
||||||
|
|
||||||
|
|
@ -683,7 +713,7 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
AtualizarTempoRestante();
|
AtualizarTempoRestante();
|
||||||
|
|
||||||
AtualizarAutonomia(distanciaTotalMetrosAtual, _dt);
|
FecharIntervaloAutonomia(_dt, deltaEnergiaDescarga_Wh);
|
||||||
|
|
||||||
AtualizarDiferencasSoc();
|
AtualizarDiferencasSoc();
|
||||||
}
|
}
|
||||||
|
|
@ -937,6 +967,9 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
_ultimaFonteIntegrada = FonteAtiva;
|
_ultimaFonteIntegrada = FonteAtiva;
|
||||||
|
|
||||||
|
_tensaoAnteriorIntegracao_V = TensaoInstantanea;
|
||||||
|
_correnteAnteriorIntegracao_A = CorrenteLiquida_A;
|
||||||
|
|
||||||
_estadoInicializado = true;
|
_estadoInicializado = true;
|
||||||
|
|
||||||
_dt = 0;
|
_dt = 0;
|
||||||
|
|
@ -945,12 +978,29 @@ namespace AgroBase.Services
|
||||||
|
|
||||||
ResetarHistoricoAutonomia(distanciaTotalMetrosAtual);
|
ResetarHistoricoAutonomia(distanciaTotalMetrosAtual);
|
||||||
|
|
||||||
AtualizarAutonomiaSemIntegrar(distanciaTotalMetrosAtual);
|
RecalcularAutonomia();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IntegrarEnergiaECarga(double dt_s)
|
private double IntegrarEnergiaECarga(
|
||||||
|
double dt_s,
|
||||||
|
FonteEnergiaSnapshot atual)
|
||||||
{
|
{
|
||||||
double deltaCargaLiquida_C = CorrenteLiquida_A * dt_s;
|
/*
|
||||||
|
* Regra trapezoidal: representa melhor todo o intervalo quando
|
||||||
|
* as leituras elétricas chegam espaçadas em 2-3 segundos.
|
||||||
|
*/
|
||||||
|
double correnteMedia_A =
|
||||||
|
(_correnteAnteriorIntegracao_A + atual.CorrenteLiquida_A) /
|
||||||
|
2.0;
|
||||||
|
|
||||||
|
double tensaoMedia_V =
|
||||||
|
(_tensaoAnteriorIntegracao_V + atual.Tensao_V) / 2.0;
|
||||||
|
|
||||||
|
double correnteDescargaMedia_A = Math.Max(0, correnteMedia_A);
|
||||||
|
double correnteCargaMedia_A = Math.Max(0, -correnteMedia_A);
|
||||||
|
|
||||||
|
double deltaCargaLiquida_C = correnteMedia_A * dt_s;
|
||||||
|
double deltaEnergiaDescarga_Wh = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Descarga positiva reduz a carga integrada.
|
* Descarga positiva reduz a carga integrada.
|
||||||
|
|
@ -958,9 +1008,9 @@ namespace AgroBase.Services
|
||||||
*/
|
*/
|
||||||
_cargaIntegradaAtual_C = Clamp(_cargaIntegradaAtual_C - deltaCargaLiquida_C, 0, CargaMaxima_C);
|
_cargaIntegradaAtual_C = Clamp(_cargaIntegradaAtual_C - deltaCargaLiquida_C, 0, CargaMaxima_C);
|
||||||
|
|
||||||
if (CorrenteDescarga_A > 0)
|
if (correnteDescargaMedia_A > 0)
|
||||||
{
|
{
|
||||||
double deltaDescarga_C = CorrenteDescarga_A * dt_s;
|
double deltaDescarga_C = correnteDescargaMedia_A * dt_s;
|
||||||
|
|
||||||
_consumidoIntegradoTotal_C = Clamp(_consumidoIntegradoTotal_C + deltaDescarga_C, 0, CargaMaxima_C);
|
_consumidoIntegradoTotal_C = Clamp(_consumidoIntegradoTotal_C + deltaDescarga_C, 0, CargaMaxima_C);
|
||||||
|
|
||||||
|
|
@ -969,14 +1019,16 @@ namespace AgroBase.Services
|
||||||
_consumidoIntegradoOperacao_C = Clamp(_consumidoIntegradoOperacao_C + deltaDescarga_C, 0, CargaMaxima_C);
|
_consumidoIntegradoOperacao_C = Clamp(_consumidoIntegradoOperacao_C + deltaDescarga_C, 0, CargaMaxima_C);
|
||||||
}
|
}
|
||||||
|
|
||||||
double deltaEnergia_Wh = PotenciaDescarga_W * dt_s / 3600.0;
|
double deltaEnergia_Wh =
|
||||||
|
tensaoMedia_V * correnteDescargaMedia_A * dt_s / 3600.0;
|
||||||
|
|
||||||
EnergiaConsumidaTotal_Wh += Math.Max(0, deltaEnergia_Wh);
|
EnergiaConsumidaTotal_Wh += Math.Max(0, deltaEnergia_Wh);
|
||||||
|
deltaEnergiaDescarga_Wh = Math.Max(0, deltaEnergia_Wh);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CorrenteCarga_A > 0)
|
if (correnteCargaMedia_A > 0)
|
||||||
{
|
{
|
||||||
double deltaCarga_C = CorrenteCarga_A * dt_s;
|
double deltaCarga_C = correnteCargaMedia_A * dt_s;
|
||||||
|
|
||||||
_carregadoIntegradoTotal_C = Clamp(_carregadoIntegradoTotal_C + deltaCarga_C, 0, CargaMaxima_C);
|
_carregadoIntegradoTotal_C = Clamp(_carregadoIntegradoTotal_C + deltaCarga_C, 0, CargaMaxima_C);
|
||||||
|
|
||||||
|
|
@ -985,10 +1037,13 @@ namespace AgroBase.Services
|
||||||
_carregadoIntegradoOperacao_C = Clamp(_carregadoIntegradoOperacao_C + deltaCarga_C, 0, CargaMaxima_C);
|
_carregadoIntegradoOperacao_C = Clamp(_carregadoIntegradoOperacao_C + deltaCarga_C, 0, CargaMaxima_C);
|
||||||
}
|
}
|
||||||
|
|
||||||
double deltaEnergiaCarga_Wh = PotenciaCarga_W * dt_s / 3600.0;
|
double deltaEnergiaCarga_Wh =
|
||||||
|
tensaoMedia_V * correnteCargaMedia_A * dt_s / 3600.0;
|
||||||
|
|
||||||
EnergiaCarregadaTotal_Wh += Math.Max(0, deltaEnergiaCarga_Wh);
|
EnergiaCarregadaTotal_Wh += Math.Max(0, deltaEnergiaCarga_Wh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return deltaEnergiaDescarga_Wh;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AtualizarEstadoEnergia()
|
private void AtualizarEstadoEnergia()
|
||||||
|
|
@ -1051,22 +1106,73 @@ namespace AgroBase.Services
|
||||||
// Autonomia por distância
|
// Autonomia por distância
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
private void AtualizarAutonomia(double distanciaTotalMetrosAtual, double deltaTempo_s)
|
private void AcumularDistanciaPendente(double distanciaTotalMetrosAtual)
|
||||||
{
|
{
|
||||||
double deltaDist_m = CalcularDeltaDistancia(distanciaTotalMetrosAtual);
|
double deltaDist_m = CalcularDeltaDistancia(distanciaTotalMetrosAtual);
|
||||||
|
|
||||||
double deltaEnergia_Wh = PotenciaDescarga_W * Math.Max(0, deltaTempo_s) / 3600.0;
|
_distanciaPendenteAutonomia_m += deltaDist_m;
|
||||||
|
}
|
||||||
|
|
||||||
AtualizarJanelaAutonomia(deltaDist_m, deltaTempo_s, deltaEnergia_Wh);
|
private void FecharIntervaloAutonomia(
|
||||||
|
double deltaTempo_s,
|
||||||
|
double deltaEnergia_Wh)
|
||||||
|
{
|
||||||
|
if (_aguardandoBaselineAutonomia)
|
||||||
|
{
|
||||||
|
DescartarIntervaloPendente("baseline_autonomia");
|
||||||
|
_aguardandoBaselineAutonomia = false;
|
||||||
|
_intervaloAutonomiaConfiavel = true;
|
||||||
|
EstadoIntervaloAutonomia = "baseline_estabelecido";
|
||||||
|
RecalcularAutonomia();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_intervaloAutonomiaConfiavel)
|
||||||
|
{
|
||||||
|
DescartarIntervaloPendente("intervalo_nao_confiavel");
|
||||||
|
_intervaloAutonomiaConfiavel = true;
|
||||||
|
EstadoIntervaloAutonomia = "recuperado";
|
||||||
|
RecalcularAutonomia();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AtualizarJanelaAutonomia(
|
||||||
|
_distanciaPendenteAutonomia_m,
|
||||||
|
deltaTempo_s,
|
||||||
|
deltaEnergia_Wh
|
||||||
|
);
|
||||||
|
|
||||||
|
_distanciaPendenteAutonomia_m = 0;
|
||||||
|
EstadoIntervaloAutonomia = "integrado";
|
||||||
|
|
||||||
RecalcularAutonomia();
|
RecalcularAutonomia();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AtualizarAutonomiaSemIntegrar(double distanciaTotalMetrosAtual)
|
private void InvalidarIntervaloAutonomia(string motivo)
|
||||||
{
|
{
|
||||||
CalcularDeltaDistancia(distanciaTotalMetrosAtual);
|
_intervaloAutonomiaConfiavel = false;
|
||||||
|
EstadoIntervaloAutonomia = motivo;
|
||||||
|
}
|
||||||
|
|
||||||
RecalcularAutonomia();
|
private void DescartarIntervaloPendente(string motivo)
|
||||||
|
{
|
||||||
|
DistanciaDescartadaAutonomia_m +=
|
||||||
|
_distanciaPendenteAutonomia_m;
|
||||||
|
|
||||||
|
_distanciaPendenteAutonomia_m = 0;
|
||||||
|
EstadoIntervaloAutonomia = motivo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrepararBaselineEletrico(
|
||||||
|
FonteEnergiaSnapshot fonte,
|
||||||
|
DateTime timestamp)
|
||||||
|
{
|
||||||
|
_ultimaFonteIntegrada = fonte.Nome;
|
||||||
|
_ultimaLeituraIntegrada = timestamp;
|
||||||
|
_tensaoAnteriorIntegracao_V = fonte.Tensao_V;
|
||||||
|
_correnteAnteriorIntegracao_A = fonte.CorrenteLiquida_A;
|
||||||
|
_dt = 0;
|
||||||
|
_intervaloAutonomiaConfiavel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double CalcularDeltaDistancia(double distanciaTotalMetrosAtual)
|
private double CalcularDeltaDistancia(double distanciaTotalMetrosAtual)
|
||||||
|
|
@ -1314,6 +1420,12 @@ namespace AgroBase.Services
|
||||||
_somaEnergiaJanela_Wh = 0;
|
_somaEnergiaJanela_Wh = 0;
|
||||||
|
|
||||||
_distanciaTotalMetrosAnterior = distanciaAtual_m;
|
_distanciaTotalMetrosAnterior = distanciaAtual_m;
|
||||||
|
_distanciaPendenteAutonomia_m = 0;
|
||||||
|
_intervaloAutonomiaConfiavel = true;
|
||||||
|
_aguardandoBaselineAutonomia = true;
|
||||||
|
|
||||||
|
DistanciaDescartadaAutonomia_m = 0;
|
||||||
|
EstadoIntervaloAutonomia = "aguardando_baseline";
|
||||||
|
|
||||||
WhPorMetroJanela = 0;
|
WhPorMetroJanela = 0;
|
||||||
WhPorMetroHistorico = 0;
|
WhPorMetroHistorico = 0;
|
||||||
|
|
@ -1349,6 +1461,9 @@ namespace AgroBase.Services
|
||||||
_ultimaFonteIntegrada = "";
|
_ultimaFonteIntegrada = "";
|
||||||
_ultimaFonteLogada = "";
|
_ultimaFonteLogada = "";
|
||||||
|
|
||||||
|
_tensaoAnteriorIntegracao_V = 0;
|
||||||
|
_correnteAnteriorIntegracao_A = 0;
|
||||||
|
|
||||||
_cargaIntegradaAtual_C = 0;
|
_cargaIntegradaAtual_C = 0;
|
||||||
CargaAtual_C = 0;
|
CargaAtual_C = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ class CameraMultispectral:
|
||||||
nome_sensor=f"{self.modelo}_imu",
|
nome_sensor=f"{self.modelo}_imu",
|
||||||
modo=self.imu_modo,
|
modo=self.imu_modo,
|
||||||
gyro_unidade="deg_s",
|
gyro_unidade="deg_s",
|
||||||
referencial_robo="r,p,y",
|
referencial_robo="-r,p,-y",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.tem_imu = False
|
self.tem_imu = False
|
||||||
|
|
@ -752,12 +752,30 @@ class CameraMultispectral:
|
||||||
if self.imu is not None:
|
if self.imu is not None:
|
||||||
self.imu.atualizar_saude()
|
self.imu.atualizar_saude()
|
||||||
|
|
||||||
conectado_ctx = (ContextoGlobalRedis.get_cameras() or {}).get(self.mx_id) is not None
|
# ============================================================
|
||||||
|
# CONFIGURAÇÃO DE ROBUSTEZ
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Quanto tempo uma degradação transitória pode persistir antes
|
||||||
|
# de ser promovida para FALHA.
|
||||||
|
#
|
||||||
|
# Como a saúde normalmente roda a cada ~500 ms:
|
||||||
|
# 1º ciclo ruim -> ALERTA
|
||||||
|
# 2º ciclo ruim -> ALERTA
|
||||||
|
# persistiu ~1 s -> FALHA
|
||||||
|
TEMPO_CONFIRMACAO_FALHA_S = 1.0
|
||||||
|
|
||||||
|
conectado_ctx = (
|
||||||
|
(ContextoGlobalRedis.get_cameras() or {}).get(self.mx_id)
|
||||||
|
is not None
|
||||||
|
)
|
||||||
|
|
||||||
motivos = []
|
motivos = []
|
||||||
saude = 50
|
saude = 50
|
||||||
|
|
||||||
status = self._safe_get_status()
|
status = self._safe_get_status()
|
||||||
running = bool(status.get("running", False))
|
running = bool(status.get("running", False))
|
||||||
|
|
||||||
conectado = bool(
|
conectado = bool(
|
||||||
self.iniciado
|
self.iniciado
|
||||||
and self.client is not None
|
and self.client is not None
|
||||||
|
|
@ -765,6 +783,10 @@ class CameraMultispectral:
|
||||||
and not self._falha_fatal_pendente
|
and not self._falha_fatal_pendente
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Falha realmente crítica de hardware/pipeline.
|
||||||
|
# Estas NÃO recebem tolerância.
|
||||||
|
falha_critica_imediata = False
|
||||||
|
|
||||||
performance = {
|
performance = {
|
||||||
"temperatura": 0.0,
|
"temperatura": 0.0,
|
||||||
"memoria_usada": 0.0,
|
"memoria_usada": 0.0,
|
||||||
|
|
@ -774,12 +796,22 @@ class CameraMultispectral:
|
||||||
"async_capture": status.get("async_capture") or {},
|
"async_capture": status.get("async_capture") or {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PIPELINE IA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
if pipeline_ia is not None:
|
if pipeline_ia is not None:
|
||||||
self._ultimo_pipeline_ia = dict(pipeline_ia)
|
self._ultimo_pipeline_ia = dict(pipeline_ia)
|
||||||
|
|
||||||
pipeline_ia = dict(self._ultimo_pipeline_ia or {})
|
pipeline_ia = dict(self._ultimo_pipeline_ia or {})
|
||||||
pipeline_ia_ok = bool(pipeline_ia.get("ok", False))
|
pipeline_ia_raw_ok = bool(pipeline_ia.get("ok", False))
|
||||||
|
|
||||||
performance["pipeline_ia"] = pipeline_ia
|
performance["pipeline_ia"] = pipeline_ia
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# IMU
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
imu = self.imu.get_dados() if self.imu else {
|
imu = self.imu.get_dados() if self.imu else {
|
||||||
"valido": False,
|
"valido": False,
|
||||||
"calibrado": False,
|
"calibrado": False,
|
||||||
|
|
@ -800,68 +832,118 @@ class CameraMultispectral:
|
||||||
|
|
||||||
resultado = dict(self._ultimo_resultado_tensor)
|
resultado = dict(self._ultimo_resultado_tensor)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MÉTRICAS DO DEVICE
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metrics = (
|
metrics = (
|
||||||
self.client.get_device_metrics()
|
self.client.get_device_metrics()
|
||||||
if self.client is not None
|
if self.client is not None
|
||||||
else {"ok": False, "error": "client indisponível"}
|
else {
|
||||||
|
"ok": False,
|
||||||
|
"error": "client indisponível",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
performance["device_metrics"] = metrics
|
performance["device_metrics"] = metrics
|
||||||
|
|
||||||
temp_raw = metrics.get("temperature_average_c")
|
temp_raw = metrics.get("temperature_average_c")
|
||||||
|
|
||||||
if temp_raw is None:
|
if temp_raw is None:
|
||||||
performance["temperatura"] = None
|
performance["temperatura"] = None
|
||||||
motivos.append("Temperatura interna indisponível")
|
motivos.append("Temperatura interna indisponível")
|
||||||
saude -= 10
|
saude -= 10
|
||||||
|
|
||||||
else:
|
else:
|
||||||
temp = float(temp_raw)
|
temp = float(temp_raw)
|
||||||
performance["temperatura"] = temp
|
performance["temperatura"] = temp
|
||||||
|
|
||||||
if temp >= 100:
|
if temp >= 100:
|
||||||
motivos.append(f"Temperatura de emergência: {temp:.1f} °C")
|
motivos.append(
|
||||||
|
f"Temperatura de emergência: {temp:.1f} °C"
|
||||||
|
)
|
||||||
saude = min(saude, 10)
|
saude = min(saude, 10)
|
||||||
|
falha_critica_imediata = True
|
||||||
|
|
||||||
elif temp >= 95:
|
elif temp >= 95:
|
||||||
motivos.append(f"Temperatura crítica: {temp:.1f} °C")
|
motivos.append(
|
||||||
|
f"Temperatura crítica: {temp:.1f} °C"
|
||||||
|
)
|
||||||
saude = min(saude, 35)
|
saude = min(saude, 35)
|
||||||
|
|
||||||
elif temp >= 85:
|
elif temp >= 85:
|
||||||
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
|
motivos.append(
|
||||||
|
f"Temperatura elevada: {temp:.1f} °C"
|
||||||
|
)
|
||||||
saude -= 15
|
saude -= 15
|
||||||
|
|
||||||
elif temp >= 75:
|
elif temp >= 75:
|
||||||
motivos.append(f"Aquecimento preventivo: {temp:.1f} °C")
|
motivos.append(
|
||||||
|
f"Aquecimento preventivo: {temp:.1f} °C"
|
||||||
|
)
|
||||||
saude -= 5
|
saude -= 5
|
||||||
|
|
||||||
ddr = float(metrics.get("ddr_used_bytes", 0) or 0) / 1024.0 / 1024.0
|
ddr = (
|
||||||
|
float(metrics.get("ddr_used_bytes", 0) or 0)
|
||||||
|
/ 1024.0
|
||||||
|
/ 1024.0
|
||||||
|
)
|
||||||
|
|
||||||
performance["memoria_usada"] = ddr
|
performance["memoria_usada"] = ddr
|
||||||
|
|
||||||
if ddr >= 500:
|
if ddr >= 500:
|
||||||
motivos.append(f"Memória DDR crítica: {ddr:.1f} MB")
|
motivos.append(
|
||||||
|
f"Memória DDR crítica: {ddr:.1f} MB"
|
||||||
|
)
|
||||||
saude -= 30
|
saude -= 30
|
||||||
|
|
||||||
elif ddr >= 400:
|
elif ddr >= 400:
|
||||||
motivos.append(f"Memória DDR elevada: {ddr:.1f} MB")
|
motivos.append(
|
||||||
|
f"Memória DDR elevada: {ddr:.1f} MB"
|
||||||
|
)
|
||||||
saude -= 15
|
saude -= 15
|
||||||
|
|
||||||
elif ddr >= 300:
|
elif ddr >= 300:
|
||||||
motivos.append(f"Memória DDR acima do ideal: {ddr:.1f} MB")
|
motivos.append(
|
||||||
|
f"Memória DDR acima do ideal: {ddr:.1f} MB"
|
||||||
|
)
|
||||||
saude -= 5
|
saude -= 5
|
||||||
|
|
||||||
speed = str(metrics.get("usb_speed", "") or "")
|
speed = str(metrics.get("usb_speed", "") or "")
|
||||||
performance["velocidade"] = speed
|
performance["velocidade"] = speed
|
||||||
|
|
||||||
if speed and speed.lower() not in ("super", "superplus"):
|
if speed and speed.lower() not in ("super", "superplus"):
|
||||||
motivos.append(f"USB lenta: {speed}")
|
motivos.append(f"USB lenta: {speed}")
|
||||||
saude -= 15
|
saude -= 15
|
||||||
|
|
||||||
async_status = metrics.get("async_capture") or performance["async_capture"]
|
async_status = (
|
||||||
|
metrics.get("async_capture")
|
||||||
|
or performance["async_capture"]
|
||||||
|
)
|
||||||
|
|
||||||
performance["async_capture"] = async_status
|
performance["async_capture"] = async_status
|
||||||
|
|
||||||
if running and async_status:
|
if running and async_status:
|
||||||
if not bool(async_status.get("thread_alive", False)):
|
if not bool(async_status.get("thread_alive", False)):
|
||||||
motivos.append("Thread assíncrona de captura parada")
|
motivos.append(
|
||||||
|
"Thread assíncrona de captura parada"
|
||||||
|
)
|
||||||
|
|
||||||
|
falha_critica_imediata = True
|
||||||
saude = 0
|
saude = 0
|
||||||
|
|
||||||
erro_async = async_status.get("last_error")
|
erro_async = async_status.get("last_error")
|
||||||
|
|
||||||
if erro_async:
|
if erro_async:
|
||||||
motivos.append(f"Captura assíncrona: {erro_async}")
|
motivos.append(
|
||||||
|
f"Captura assíncrona: {erro_async}"
|
||||||
|
)
|
||||||
|
|
||||||
if not running:
|
if not running:
|
||||||
motivos.append("Pipeline parada")
|
motivos.append("Pipeline parada")
|
||||||
saude -= 20
|
falha_critica_imediata = True
|
||||||
|
saude = 0
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
resultado = {
|
resultado = {
|
||||||
|
|
@ -869,66 +951,243 @@ class CameraMultispectral:
|
||||||
"duracao": 0.0,
|
"duracao": 0.0,
|
||||||
"frame_valido": False,
|
"frame_valido": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._is_erro_fatal_depthai(e):
|
if self._is_erro_fatal_depthai(e):
|
||||||
conectado = False
|
conectado = False
|
||||||
|
falha_critica_imediata = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TENSOR
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
agora = time.time()
|
agora = time.time()
|
||||||
|
agora_mono = time.monotonic()
|
||||||
|
|
||||||
ts_tensor = self.timestamp_ultimo_tensor or 0.0
|
ts_tensor = self.timestamp_ultimo_tensor or 0.0
|
||||||
frame_recente = ts_tensor > 0 and (agora - ts_tensor) <= 2.0
|
|
||||||
|
idade_tensor = (
|
||||||
|
agora - ts_tensor
|
||||||
|
if ts_tensor > 0
|
||||||
|
else float("inf")
|
||||||
|
)
|
||||||
|
|
||||||
|
frame_recente_raw = (
|
||||||
|
ts_tensor > 0
|
||||||
|
and idade_tensor <= 2.0
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
performance["sync_dt_ms"] = float(resultado.get("sync_dt_ms", 0.0) or 0.0)
|
performance["sync_dt_ms"] = float(
|
||||||
|
resultado.get("sync_dt_ms", 0.0) or 0.0
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PERSISTÊNCIA DO TENSOR ATRASADO
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
tensor_falha_confirmada = False
|
||||||
|
tensor_degradado = False
|
||||||
|
|
||||||
|
if frame_recente_raw:
|
||||||
|
# Recuperou -> limpa imediatamente a persistência.
|
||||||
|
self._tensor_atrasado_desde = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
tensor_degradado = True
|
||||||
|
|
||||||
|
inicio = getattr(
|
||||||
|
self,
|
||||||
|
"_tensor_atrasado_desde",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if inicio is None:
|
||||||
|
self._tensor_atrasado_desde = agora_mono
|
||||||
|
inicio = agora_mono
|
||||||
|
|
||||||
|
duracao_atraso = agora_mono - inicio
|
||||||
|
|
||||||
|
if duracao_atraso >= TEMPO_CONFIRMACAO_FALHA_S:
|
||||||
|
tensor_falha_confirmada = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ESTADO BASE DO TENSOR / CÂMERA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
if not conectado and not conectado_ctx:
|
if not conectado and not conectado_ctx:
|
||||||
motivos.append("desconectado")
|
motivos.append("desconectado")
|
||||||
saude = 0
|
saude = 0
|
||||||
|
falha_critica_imediata = True
|
||||||
|
|
||||||
elif resultado.get("erro"):
|
elif resultado.get("erro"):
|
||||||
motivos.append(resultado["erro"])
|
motivos.append(resultado["erro"])
|
||||||
saude = 0
|
saude = 0
|
||||||
|
falha_critica_imediata = True
|
||||||
|
|
||||||
elif not resultado.get("frame_valido", False):
|
elif not resultado.get("frame_valido", False):
|
||||||
motivos.append("Tensor inválido ou indisponível")
|
motivos.append("Tensor inválido ou indisponível")
|
||||||
saude = 0
|
saude = 0
|
||||||
|
falha_critica_imediata = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
saude += 50
|
saude += 50
|
||||||
|
|
||||||
if not frame_recente:
|
if tensor_degradado:
|
||||||
motivos.append(f"Tensor sem atualização há {agora - ts_tensor:.2f}s")
|
motivos.append(
|
||||||
saude = 0
|
f"Tensor sem atualização há {idade_tensor:.2f}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
if tensor_falha_confirmada:
|
||||||
|
saude = 0
|
||||||
|
else:
|
||||||
|
# Existe problema, mas ainda é transitório.
|
||||||
|
# Mantemos como ALERTA.
|
||||||
|
saude = min(saude, 70)
|
||||||
|
|
||||||
|
dur = float(
|
||||||
|
resultado.get("duracao", 0.0) or 0.0
|
||||||
|
)
|
||||||
|
|
||||||
dur = float(resultado.get("duracao", 0.0) or 0.0)
|
|
||||||
if dur > 1.5:
|
if dur > 1.5:
|
||||||
saude -= 20
|
saude -= 20
|
||||||
motivos.append(f"Tempo elevado para captura tensor: {dur:.2f}s")
|
motivos.append(
|
||||||
|
f"Tempo elevado para captura tensor: {dur:.2f}s"
|
||||||
|
)
|
||||||
|
|
||||||
elif dur > 0.8:
|
elif dur > 0.8:
|
||||||
saude -= 10
|
saude -= 10
|
||||||
motivos.append(f"Tempo moderado para captura tensor: {dur:.2f}s")
|
motivos.append(
|
||||||
|
f"Tempo moderado para captura tensor: {dur:.2f}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
sync_dt = float(
|
||||||
|
resultado.get("sync_dt_ms", 0.0) or 0.0
|
||||||
|
)
|
||||||
|
|
||||||
sync_dt = float(resultado.get("sync_dt_ms", 0.0) or 0.0)
|
|
||||||
if sync_dt > 35.0:
|
if sync_dt > 35.0:
|
||||||
saude -= 10
|
saude -= 10
|
||||||
motivos.append(f"Sincronismo alto: {sync_dt:.1f} ms")
|
motivos.append(
|
||||||
|
f"Sincronismo alto: {sync_dt:.1f} ms"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PERSISTÊNCIA DO PIPELINE IA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
pipeline_falha_confirmada = False
|
||||||
|
pipeline_degradado = False
|
||||||
|
|
||||||
|
if pipeline_ia_raw_ok:
|
||||||
|
# Recuperou -> limpa imediatamente.
|
||||||
|
self._pipeline_ia_falha_desde = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
pipeline_degradado = True
|
||||||
|
|
||||||
if not pipeline_ia_ok:
|
|
||||||
for motivo in pipeline_ia.get("motivos", []) or []:
|
for motivo in pipeline_ia.get("motivos", []) or []:
|
||||||
texto = f"Pipeline IA: {motivo}"
|
texto = f"Pipeline IA: {motivo}"
|
||||||
|
|
||||||
if texto not in motivos:
|
if texto not in motivos:
|
||||||
motivos.append(texto)
|
motivos.append(texto)
|
||||||
saude = 0
|
|
||||||
|
|
||||||
motivos = list(dict.fromkeys(str(m) for m in motivos if m))
|
inicio = getattr(
|
||||||
|
self,
|
||||||
|
"_pipeline_ia_falha_desde",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if inicio is None:
|
||||||
|
self._pipeline_ia_falha_desde = agora_mono
|
||||||
|
inicio = agora_mono
|
||||||
|
|
||||||
|
duracao_falha = agora_mono - inicio
|
||||||
|
|
||||||
|
if duracao_falha >= TEMPO_CONFIRMACAO_FALHA_S:
|
||||||
|
pipeline_falha_confirmada = True
|
||||||
|
saude = 0
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Não escondemos a degradação.
|
||||||
|
# Só impedimos que vire FALHA imediatamente.
|
||||||
|
saude = min(saude, 70)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DIAGNÓSTICO DE PERSISTÊNCIA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
performance["watchdog"] = {
|
||||||
|
"tensor_degradado": tensor_degradado,
|
||||||
|
"tensor_falha_confirmada": tensor_falha_confirmada,
|
||||||
|
"pipeline_degradado": pipeline_degradado,
|
||||||
|
"pipeline_falha_confirmada": pipeline_falha_confirmada,
|
||||||
|
"tempo_confirmacao_s": TEMPO_CONFIRMACAO_FALHA_S,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# RESULTADO FINAL
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
motivos = list(
|
||||||
|
dict.fromkeys(
|
||||||
|
str(m)
|
||||||
|
for m in motivos
|
||||||
|
if m
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
saude = min(max(saude, 0), 100)
|
saude = min(max(saude, 0), 100)
|
||||||
|
|
||||||
status_mod = StatusModulo.OPERANTE
|
# Durante a janela de tolerância, consideramos o pipeline
|
||||||
|
# operacional, mas degradado.
|
||||||
|
frame_ok_operacional = (
|
||||||
|
frame_recente_raw
|
||||||
|
or (
|
||||||
|
tensor_degradado
|
||||||
|
and not tensor_falha_confirmada
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
pipeline_ok_operacional = (
|
||||||
|
pipeline_ia_raw_ok
|
||||||
|
or (
|
||||||
|
pipeline_degradado
|
||||||
|
and not pipeline_falha_confirmada
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# STATUS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
if not conectado:
|
if not conectado:
|
||||||
status_mod = StatusModulo.DESCONECTADO
|
status_mod = StatusModulo.DESCONECTADO
|
||||||
elif not frame_recente or not pipeline_ia_ok or saude <= 0:
|
|
||||||
|
elif (
|
||||||
|
falha_critica_imediata
|
||||||
|
or tensor_falha_confirmada
|
||||||
|
or pipeline_falha_confirmada
|
||||||
|
):
|
||||||
status_mod = StatusModulo.FALHA
|
status_mod = StatusModulo.FALHA
|
||||||
elif saude < 80:
|
|
||||||
|
elif (
|
||||||
|
tensor_degradado
|
||||||
|
or pipeline_degradado
|
||||||
|
or saude < 80
|
||||||
|
):
|
||||||
status_mod = StatusModulo.ALERTA
|
status_mod = StatusModulo.ALERTA
|
||||||
|
|
||||||
self.rodando = bool(conectado and frame_recente and pipeline_ia_ok)
|
else:
|
||||||
|
status_mod = StatusModulo.OPERANTE
|
||||||
|
|
||||||
|
# Durante ALERTA transitório continua rodando.
|
||||||
|
self.rodando = bool(
|
||||||
|
conectado
|
||||||
|
and frame_ok_operacional
|
||||||
|
and pipeline_ok_operacional
|
||||||
|
and not falha_critica_imediata
|
||||||
|
)
|
||||||
|
|
||||||
saude_geral = {
|
saude_geral = {
|
||||||
"timestamp": agora,
|
"timestamp": agora,
|
||||||
|
|
@ -938,6 +1197,7 @@ class CameraMultispectral:
|
||||||
"motivos": motivos,
|
"motivos": motivos,
|
||||||
"saude_individual": [],
|
"saude_individual": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ultima_saude = saude_geral
|
self.ultima_saude = saude_geral
|
||||||
|
|
||||||
from camera_worker.manager import definir_saude_camera
|
from camera_worker.manager import definir_saude_camera
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ def main():
|
||||||
T_Code.Sen: ModuloSensoriamento(),
|
T_Code.Sen: ModuloSensoriamento(),
|
||||||
T_Code.Atu: ModuloAtuador(),
|
T_Code.Atu: ModuloAtuador(),
|
||||||
#T_Code.Lra: ModuloLoRa(),
|
#T_Code.Lra: ModuloLoRa(),
|
||||||
T_Code.Imu: CameraIMU(modulos_imu=[T_Code.Snr, T_Code.Cam]),
|
T_Code.Imu: CameraIMU(modulos_imu=[T_Code.Snr]),
|
||||||
T_Code.Npc: ModuloPC(),
|
T_Code.Npc: ModuloPC(),
|
||||||
T_Code.Lvx: ModuloLivox(),
|
T_Code.Lvx: ModuloLivox(),
|
||||||
T_Code.Ipb: ModuloIPBribge(),
|
T_Code.Ipb: ModuloIPBribge(),
|
||||||
|
|
|
||||||
|
|
@ -1,356 +1,628 @@
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from shared.enums import T_Code, StatusModulo
|
from shared.enums import T_Code, StatusModulo
|
||||||
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
|
from shared.contexto_global_redis import ContextoGlobalRedis
|
||||||
from health_worker.modulos.base import ModuloDiagnosticoBase
|
from health_worker.modulos.base import ModuloDiagnosticoBase
|
||||||
|
|
||||||
|
|
||||||
class ModuloGPS(ModuloDiagnosticoBase):
|
class ModuloGPS(ModuloDiagnosticoBase):
|
||||||
|
"""Health do UM982 preparado para operacao real de campo.
|
||||||
|
|
||||||
|
Principios:
|
||||||
|
- perda de pacote/frequencia momentanea nao derruba a saude;
|
||||||
|
- ausencia REAL de GGA/THS continua sendo tratada como falha;
|
||||||
|
- qualidade RTK e fonte da correcao sao coisas diferentes;
|
||||||
|
- alertas entram e saem com persistencia/histerese;
|
||||||
|
- frequencia e avaliada por faixas, nao por diferenca percentual do alvo;
|
||||||
|
- NTRIP e apenas fallback. NTRIP desligado nao e defeito se o RTK esta bom.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Recepcao das sentencas
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
RX_WARN_RATIO = 0.60 # abaixo de 60% da freq_base pode gerar alerta
|
||||||
|
RX_FAIL_AGE_S = 3.0 # nenhuma sentenca por 3 s = candidato a falha
|
||||||
|
RX_FAIL_PERSIST_S = 2.0 # confirma perda somente se persistir
|
||||||
|
RX_RECOVER_S = 2.0 # exige dados frescos por 2 s para limpar falha
|
||||||
|
|
||||||
|
FREQ_WARN_PERSIST_S = 8.0 # frequencia baixa precisa durar 8 s
|
||||||
|
FREQ_RECOVER_S = 4.0 # recuperacao sustentada antes de limpar alerta
|
||||||
|
FREQ_PENALTY = 8 # penalidade pequena; freshness e mais importante
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Heading / THS
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
ORI_INVALID_PERSIST_S = 2.0
|
||||||
|
ORI_RECOVER_S = 2.0
|
||||||
|
ORI_INVALID_PENALTY = 40
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# RTK
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
AGE_GOOD_S = 3.0
|
||||||
|
AGE_WARN_S = 5.0
|
||||||
|
AGE_FAIL_S = 15.0
|
||||||
|
RTK_FAIL_PERSIST_S = 3.0
|
||||||
|
RTK_RECOVER_S = 4.0
|
||||||
|
|
||||||
|
FIX_BAD_PERSIST_S = 3.0
|
||||||
|
FIX_RECOVER_S = 3.0
|
||||||
|
|
||||||
|
HACC_WARN_CM = 40.0
|
||||||
|
HACC_BAD_CM = 120.0
|
||||||
|
HACC_PERSIST_S = 5.0
|
||||||
|
HACC_RECOVER_S = 3.0
|
||||||
|
|
||||||
|
SAUDE_MIN_ALERTA = 80
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.t_code = T_Code.Gps
|
self.t_code = T_Code.Gps
|
||||||
self.nome = "UM982"
|
self.nome = "UM982"
|
||||||
self.timeout = 5
|
self.timeout = 5
|
||||||
|
|
||||||
self._rtk_expirado_desde = 0.0
|
# Cada condicao possui latch proprio. Assim um unico frame ruim nao
|
||||||
self._rtk_recuperado_desde = 0.0
|
# cria um alerta e um unico frame bom tambem nao mascara uma falha.
|
||||||
self._rtk_falha_persistente = False
|
self._persist = {}
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# API PRINCIPAL
|
||||||
|
# ==================================================================
|
||||||
def atualizar_saude(self):
|
def atualizar_saude(self):
|
||||||
try:
|
try:
|
||||||
now = time.perf_counter()
|
agora_mono = time.monotonic()
|
||||||
modulo = ContextoGlobalRedis.get_modulo(self.t_code) or {}
|
modulo = ContextoGlobalRedis.get_modulo(self.t_code) or {}
|
||||||
FREQ_BASE = modulo.get("freq_base", 1)
|
|
||||||
FREQ_MIN = FREQ_BASE * 0.2
|
|
||||||
SAUDE_MIN_ALERTA = 80
|
|
||||||
|
|
||||||
conectado = modulo.get("conectado", False)
|
conectado = self._bool(modulo.get("conectado", False))
|
||||||
freq_pos = modulo.get("freq", {}).get("posicao", 0.0)
|
freq_base = self._float(modulo.get("freq_base", 1.0), 1.0)
|
||||||
freq_ori = modulo.get("freq", {}).get("orientacao", 0.0)
|
if not math.isfinite(freq_base) or freq_base <= 0:
|
||||||
ts_pos = modulo.get("timestamp", {}).get("posicao", 0.0)
|
freq_base = 1.0
|
||||||
ts_ori = modulo.get("timestamp", {}).get("orientacao", 0.0)
|
|
||||||
fail_pos = (now - ts_pos) > 2.0
|
freq = modulo.get("freq") or {}
|
||||||
fail_ori = (now - ts_ori) > 2.0
|
timestamps = modulo.get("timestamp") or {}
|
||||||
fix = modulo.get("fix", 0)
|
|
||||||
rtk = modulo.get("rtk", False)
|
freq_pos = max(0.0, self._float(freq.get("posicao", 0.0), 0.0))
|
||||||
hAcc = modulo.get("hAcc", 0)
|
freq_ori = max(0.0, self._float(freq.get("orientacao", 0.0), 0.0))
|
||||||
age_bruto = modulo.get("age", -1)
|
ts_pos = self._float(timestamps.get("posicao", 0.0), 0.0)
|
||||||
ntrip = modulo.get("correcao_ntrip", False)
|
ts_ori = self._float(timestamps.get("orientacao", 0.0), 0.0)
|
||||||
|
|
||||||
|
idade_pos = self._idade_timestamp(ts_pos)
|
||||||
|
idade_ori = self._idade_timestamp(ts_ori)
|
||||||
|
|
||||||
|
fix = self._int(modulo.get("fix", 0), 0)
|
||||||
|
rtk = self._bool(modulo.get("rtk", False))
|
||||||
|
hacc = self._float(modulo.get("hAcc", 0.0), 0.0)
|
||||||
|
age = self._float(modulo.get("age", -1.0), -1.0)
|
||||||
|
age_valido = math.isfinite(age) and age >= 0.0
|
||||||
|
|
||||||
|
# correcao_ntrip descreve A FONTE usada como fallback, nao a
|
||||||
|
# qualidade da solucao GNSS. Nunca penalizar a saude so por False.
|
||||||
|
correcao_ntrip = self._bool(modulo.get("correcao_ntrip", False))
|
||||||
|
|
||||||
orientacao = modulo.get("orientacao") or {}
|
orientacao = modulo.get("orientacao") or {}
|
||||||
status_ori = str(orientacao.get("status", "")).strip().upper()
|
status_ori = str(orientacao.get("status", "")).strip().upper()
|
||||||
valor_ori_bruto = orientacao.get("valor", 9999.0)
|
valor_ori = self._float(orientacao.get("valor", math.nan), math.nan)
|
||||||
valida_redis = orientacao.get("valida", False)
|
valida_ori = self._bool(orientacao.get("valida", False))
|
||||||
|
valor_ori_valido = math.isfinite(valor_ori) and 0.0 <= valor_ori < 360.0
|
||||||
|
|
||||||
try:
|
# ----------------------------------------------------------
|
||||||
age = float(age_bruto)
|
# Persistencias / latches
|
||||||
except (TypeError, ValueError):
|
# ----------------------------------------------------------
|
||||||
age = -1.0
|
perda_pos = self._persistente(
|
||||||
|
"perda_pos",
|
||||||
|
ruim=idade_pos > self.RX_FAIL_AGE_S,
|
||||||
|
agora=agora_mono,
|
||||||
|
entrar_s=self.RX_FAIL_PERSIST_S,
|
||||||
|
sair_s=self.RX_RECOVER_S,
|
||||||
|
)
|
||||||
|
perda_ori = self._persistente(
|
||||||
|
"perda_ori",
|
||||||
|
ruim=idade_ori > self.RX_FAIL_AGE_S,
|
||||||
|
agora=agora_mono,
|
||||||
|
entrar_s=self.RX_FAIL_PERSIST_S,
|
||||||
|
sair_s=self.RX_RECOVER_S,
|
||||||
|
)
|
||||||
|
|
||||||
AGE_WARN = 5.0
|
freq_min_alerta = freq_base * self.RX_WARN_RATIO
|
||||||
AGE_FAIL = 15.0
|
freq_pos_baixa = self._persistente(
|
||||||
AGE_RECOVER = 3.0
|
"freq_pos_baixa",
|
||||||
T_FAIL = 2.0
|
ruim=(idade_pos <= self.RX_FAIL_AGE_S and freq_pos < freq_min_alerta),
|
||||||
T_RECOVER = 3.0
|
agora=agora_mono,
|
||||||
agora_mono = time.monotonic()
|
entrar_s=self.FREQ_WARN_PERSIST_S,
|
||||||
age_valido = math.isfinite(age) and age >= 0.0
|
sair_s=self.FREQ_RECOVER_S,
|
||||||
correcao_expirada = not age_valido or age >= AGE_FAIL
|
)
|
||||||
correcao_recuperada = (age_valido and age <= AGE_RECOVER and bool(rtk))
|
freq_ori_baixa = self._persistente(
|
||||||
if correcao_expirada:
|
"freq_ori_baixa",
|
||||||
if self._rtk_expirado_desde <= 0:
|
ruim=(idade_ori <= self.RX_FAIL_AGE_S and freq_ori < freq_min_alerta),
|
||||||
self._rtk_expirado_desde = agora_mono
|
agora=agora_mono,
|
||||||
self._rtk_recuperado_desde = 0.0
|
entrar_s=self.FREQ_WARN_PERSIST_S,
|
||||||
if (agora_mono - self._rtk_expirado_desde >= T_FAIL):
|
sair_s=self.FREQ_RECOVER_S,
|
||||||
self._rtk_falha_persistente = True
|
)
|
||||||
elif correcao_recuperada:
|
|
||||||
self._rtk_expirado_desde = 0.0
|
|
||||||
if self._rtk_recuperado_desde <= 0:
|
|
||||||
self._rtk_recuperado_desde = agora_mono
|
|
||||||
if (agora_mono - self._rtk_recuperado_desde >= T_RECOVER):
|
|
||||||
self._rtk_falha_persistente = False
|
|
||||||
else:
|
|
||||||
self._rtk_expirado_desde = 0.0
|
|
||||||
self._rtk_recuperado_desde = 0.0
|
|
||||||
|
|
||||||
# Proteção caso o Redis devolva booleano como texto.
|
ori_invalida_agora = not (
|
||||||
if isinstance(valida_redis, str):
|
idade_ori <= self.RX_FAIL_AGE_S
|
||||||
valida_redis = valida_redis.strip().lower() in (
|
and valida_ori
|
||||||
"true",
|
and status_ori == "A"
|
||||||
"1",
|
and valor_ori_valido
|
||||||
"yes",
|
)
|
||||||
"sim",
|
ori_invalida = self._persistente(
|
||||||
)
|
"ori_invalida",
|
||||||
else:
|
ruim=ori_invalida_agora and not perda_ori,
|
||||||
valida_redis = bool(valida_redis)
|
agora=agora_mono,
|
||||||
|
entrar_s=self.ORI_INVALID_PERSIST_S,
|
||||||
|
sair_s=self.ORI_RECOVER_S,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
# Fix 4 = RTK Fixed; 5 = RTK Float. Float e degradado, nao falha.
|
||||||
valor_ori = float(valor_ori_bruto)
|
fix_ruim = fix not in (4, 5)
|
||||||
except (TypeError, ValueError):
|
fix_ruim_persistente = self._persistente(
|
||||||
valor_ori = 9999.0
|
"fix_ruim",
|
||||||
|
ruim=fix_ruim,
|
||||||
|
agora=agora_mono,
|
||||||
|
entrar_s=self.FIX_BAD_PERSIST_S,
|
||||||
|
sair_s=self.FIX_RECOVER_S,
|
||||||
|
)
|
||||||
|
|
||||||
valor_ori_valido = (math.isfinite(valor_ori) and 0.0 <= valor_ori < 360.0)
|
correcao_expirada = (not age_valido) or age >= self.AGE_FAIL_S
|
||||||
orientacao_recebendo = (freq_ori > FREQ_MIN and not fail_ori)
|
rtk_falha_persistente = self._persistente(
|
||||||
posicao_recebendo = (freq_pos > FREQ_MIN and not fail_pos)
|
"rtk_expirado",
|
||||||
orientacao_valida = (orientacao_recebendo and valida_redis and status_ori == "A" and valor_ori_valido)
|
ruim=correcao_expirada,
|
||||||
|
agora=agora_mono,
|
||||||
|
entrar_s=self.RTK_FAIL_PERSIST_S,
|
||||||
|
sair_s=self.RTK_RECOVER_S,
|
||||||
|
)
|
||||||
|
|
||||||
saude = 100
|
hacc_ruim_agora = (
|
||||||
|
(not math.isfinite(hacc))
|
||||||
|
or hacc <= 0.0
|
||||||
|
or hacc >= self.HACC_WARN_CM
|
||||||
|
)
|
||||||
|
hacc_ruim = self._persistente(
|
||||||
|
"hacc_ruim",
|
||||||
|
ruim=hacc_ruim_agora,
|
||||||
|
agora=agora_mono,
|
||||||
|
entrar_s=self.HACC_PERSIST_S,
|
||||||
|
sair_s=self.HACC_RECOVER_S,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Score de saude
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
saude = 100.0
|
||||||
motivos = []
|
motivos = []
|
||||||
|
|
||||||
if not conectado:
|
if not conectado:
|
||||||
|
saude = 0.0
|
||||||
motivos.append("GNSS desconectado")
|
motivos.append("GNSS desconectado")
|
||||||
saude = 0
|
|
||||||
|
|
||||||
elif not posicao_recebendo:
|
|
||||||
motivos.append("Sem resposta de coordenadas GGA")
|
|
||||||
saude = 0
|
|
||||||
|
|
||||||
elif not orientacao_recebendo:
|
|
||||||
motivos.append("Sem resposta da sentença de orientação THS")
|
|
||||||
saude = 0
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
f_pos = min(freq_pos, FREQ_BASE)
|
# Ausencia persistente de sentenca e falha real de comunicacao.
|
||||||
erro_pos = (FREQ_BASE - f_pos) / FREQ_BASE
|
if perda_pos:
|
||||||
p_freq_pos = min(int(erro_pos * 100), 40)
|
saude = 0.0
|
||||||
saude -= abs(p_freq_pos)
|
motivos.append(
|
||||||
if abs(p_freq_pos) > 0:
|
f"Sem coordenadas GGA recentes ha {idade_pos:.1f}s"
|
||||||
motivos.append(f"Frequência de coordenadas baixa: {f_pos:.2f} Hz (penalidade {p_freq_pos}%)")
|
)
|
||||||
|
|
||||||
f_ori = min(freq_ori, FREQ_BASE)
|
if perda_ori:
|
||||||
erro_ori = (FREQ_BASE - f_ori) / FREQ_BASE
|
saude = 0.0
|
||||||
p_freq_ori = min(int(erro_ori * 100), 40)
|
motivos.append(
|
||||||
saude -= abs(p_freq_ori)
|
f"Sem orientacao THS recente ha {idade_ori:.1f}s"
|
||||||
if abs(p_freq_ori) > 0:
|
)
|
||||||
motivos.append(f"Frequência de orientacao baixa: {f_ori:.2f} Hz (penalidade {p_freq_ori}%)")
|
|
||||||
|
|
||||||
# A sentença está chegando, mas a solução pode estar inválida.
|
|
||||||
if not orientacao_valida:
|
|
||||||
P_ORIENTACAO_INVALIDA = 45
|
|
||||||
saude -= P_ORIENTACAO_INVALIDA
|
|
||||||
|
|
||||||
|
# Frequencia baixa isoladamente e diagnostico leve. O dado
|
||||||
|
# continua util se esta chegando fresco.
|
||||||
|
if not perda_pos and freq_pos_baixa:
|
||||||
|
saude -= self.FREQ_PENALTY
|
||||||
|
motivos.append(
|
||||||
|
"Frequencia GGA persistentemente baixa: "
|
||||||
|
f"{freq_pos:.2f} Hz (esperado ~{freq_base:.2f} Hz, "
|
||||||
|
f"penalidade {self.FREQ_PENALTY}%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not perda_ori and freq_ori_baixa:
|
||||||
|
saude -= self.FREQ_PENALTY
|
||||||
|
motivos.append(
|
||||||
|
"Frequencia THS persistentemente baixa: "
|
||||||
|
f"{freq_ori:.2f} Hz (esperado ~{freq_base:.2f} Hz, "
|
||||||
|
f"penalidade {self.FREQ_PENALTY}%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not perda_ori and ori_invalida:
|
||||||
|
saude -= self.ORI_INVALID_PENALTY
|
||||||
if status_ori == "V":
|
if status_ori == "V":
|
||||||
motivos.append(
|
detalhe = "status V"
|
||||||
"Sentença THS recebida, mas solução de heading inválida "
|
|
||||||
f"(status V, penalidade {P_ORIENTACAO_INVALIDA}%)"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif not valor_ori_valido:
|
elif not valor_ori_valido:
|
||||||
motivos.append(
|
detalhe = f"heading invalido ({valor_ori})"
|
||||||
"Sentença THS recebida com valor de heading inválido "
|
elif not valida_ori:
|
||||||
f"({valor_ori}, penalidade {P_ORIENTACAO_INVALIDA}%)"
|
detalhe = "orientacao nao marcada como valida"
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
motivos.append(
|
detalhe = f"status inesperado ({status_ori or 'vazio'})"
|
||||||
"Sentença THS recebida, mas orientação não foi marcada "
|
|
||||||
f"como válida (penalidade {P_ORIENTACAO_INVALIDA}%)"
|
motivos.append(
|
||||||
|
"Solucao de heading THS persistentemente invalida: "
|
||||||
|
f"{detalhe} (penalidade {self.ORI_INVALID_PENALTY}%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fix: so penaliza depois de persistir. RTK Float recebe uma
|
||||||
|
# penalidade leve mesmo sendo utilizavel.
|
||||||
|
if fix == 5:
|
||||||
|
saude -= 5
|
||||||
|
motivos.append("Solucao RTK Float (penalidade 5%)")
|
||||||
|
elif fix_ruim_persistente:
|
||||||
|
p_fix = self._penalidade_fix(fix)
|
||||||
|
saude -= p_fix
|
||||||
|
motivos.append(
|
||||||
|
f"Fix GNSS inadequado persistente: {fix} "
|
||||||
|
f"(penalidade {p_fix}%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Age e avaliado em faixas. Nao descontamos continuamente
|
||||||
|
# desde age=0 e nao citamos ntrip=False como defeito.
|
||||||
|
if rtk_falha_persistente:
|
||||||
|
saude = 0.0
|
||||||
|
detalhe_age = f"{age:.1f}s" if age_valido else "indisponivel"
|
||||||
|
motivos.append(
|
||||||
|
"Correcao RTK expirada persistentemente: "
|
||||||
|
f"age={detalhe_age}, limite={self.AGE_FAIL_S:.1f}s"
|
||||||
|
)
|
||||||
|
elif age_valido and age >= self.AGE_WARN_S:
|
||||||
|
p_age = self._penalidade_age(age)
|
||||||
|
saude -= p_age
|
||||||
|
fonte = "NTRIP" if correcao_ntrip else "correcao primaria/radio"
|
||||||
|
motivos.append(
|
||||||
|
f"Correcoes RTK envelhecidas: age={age:.1f}s "
|
||||||
|
f"(fonte={fonte}, penalidade {p_age}%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# rtk=False sozinho nao zera saude. O fix e o age dizem se a
|
||||||
|
# solucao de fato esta ruim. Evita dupla/tripla penalizacao.
|
||||||
|
if not rtk and not fix_ruim_persistente and not rtk_falha_persistente:
|
||||||
|
saude -= 5
|
||||||
|
motivos.append(
|
||||||
|
"Flag RTK inativa apesar de fix/correcao ainda utilizaveis "
|
||||||
|
"(penalidade 5%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if hacc_ruim:
|
||||||
|
p_hacc = self._penalidade_hacc(hacc)
|
||||||
|
if p_hacc > 0:
|
||||||
|
saude -= p_hacc
|
||||||
|
hacc_txt = (
|
||||||
|
f"{hacc:.2f} cm"
|
||||||
|
if math.isfinite(hacc) and hacc > 0
|
||||||
|
else "indisponivel"
|
||||||
|
)
|
||||||
|
motivos.append(
|
||||||
|
f"hAcc persistentemente degradado: {hacc_txt} "
|
||||||
|
f"(penalidade {p_hacc}%)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 🔹 Fix
|
|
||||||
|
|
||||||
p_fix = self._penalidade_fix(fix)
|
|
||||||
saude -= p_fix
|
|
||||||
if fix < 4:
|
|
||||||
motivos.append(f"Fix inadequado: {fix} (penalidade {p_fix}%)")
|
|
||||||
# 🔹 RTK
|
|
||||||
p_age = self._penalidade_age(age)
|
|
||||||
if not rtk:
|
|
||||||
p_rtk = max(20, p_age)
|
|
||||||
saude -= p_rtk
|
|
||||||
motivos.append(f"RTK inativo (penalidade {p_rtk}%; age={age:.1f}s)")
|
|
||||||
else:
|
|
||||||
# RTK reporta ativo, mas pode estar degradado se age alto
|
|
||||||
if p_age > 0:
|
|
||||||
saude -= p_age
|
|
||||||
motivos.append(f"Correção RTK degradada (age={age:.1f}s, penalidade {p_age}%, ntrip: {ntrip})")
|
|
||||||
|
|
||||||
if self._rtk_falha_persistente:
|
|
||||||
saude = 0
|
|
||||||
motivos.append(f"Correção RTK expirada persistentemente: age={age:.1f}s, limite={AGE_FAIL:.1f}s")
|
|
||||||
|
|
||||||
# 🔹 hAcc
|
saude = max(0.0, min(100.0, round(saude, 1)))
|
||||||
p_hacc = self._penalidade_hacc(hAcc)
|
|
||||||
if p_hacc > 0:
|
|
||||||
saude -= p_hacc
|
|
||||||
motivos.append(f"hAcc {'nao definido' if hAcc == 0 else 'alto'}: {hAcc:.2f} cm (penalidade {p_hacc}%)")
|
|
||||||
|
|
||||||
saude = max(round(saude, 1), 0)
|
|
||||||
|
|
||||||
if not conectado:
|
if not conectado:
|
||||||
status = StatusModulo.DESCONECTADO
|
status = StatusModulo.DESCONECTADO
|
||||||
|
elif perda_pos or perda_ori or saude <= 0:
|
||||||
elif not posicao_recebendo or not orientacao_recebendo:
|
|
||||||
status = StatusModulo.FALHA
|
status = StatusModulo.FALHA
|
||||||
|
elif saude < self.SAUDE_MIN_ALERTA:
|
||||||
elif saude <= 0:
|
|
||||||
status = StatusModulo.FALHA
|
|
||||||
|
|
||||||
elif saude < SAUDE_MIN_ALERTA:
|
|
||||||
status = StatusModulo.ALERTA
|
status = StatusModulo.ALERTA
|
||||||
|
|
||||||
else:
|
else:
|
||||||
status = StatusModulo.OPERANTE
|
status = StatusModulo.OPERANTE
|
||||||
|
|
||||||
saude_geral = {
|
saude_geral = {
|
||||||
"conectado": conectado,
|
"conectado": conectado,
|
||||||
"status": status.value,
|
"status": status.value,
|
||||||
"saude": saude,
|
"saude": saude,
|
||||||
"motivos": motivos,
|
"motivos": motivos,
|
||||||
"saude_idividual": []
|
"saude_idividual": [],
|
||||||
|
# Diagnostico adicional. Nao interfere na interface antiga e
|
||||||
|
# facilita entender o comportamento no proximo log de campo.
|
||||||
|
"diagnostico": {
|
||||||
|
"freq_base_hz": round(freq_base, 3),
|
||||||
|
"freq_pos_hz": round(freq_pos, 3),
|
||||||
|
"freq_ori_hz": round(freq_ori, 3),
|
||||||
|
"idade_pos_s": self._round_age(idade_pos),
|
||||||
|
"idade_ori_s": self._round_age(idade_ori),
|
||||||
|
"fix": fix,
|
||||||
|
"rtk": rtk,
|
||||||
|
"age_s": round(age, 2) if age_valido else None,
|
||||||
|
"hacc_cm": round(hacc, 2) if math.isfinite(hacc) and hacc > 0 else None,
|
||||||
|
"heading_valido": not ori_invalida,
|
||||||
|
"correcao_ntrip": correcao_ntrip,
|
||||||
|
"fonte_correcao": "ntrip" if correcao_ntrip else "primaria_radio",
|
||||||
|
"persistencias": {
|
||||||
|
"perda_pos": perda_pos,
|
||||||
|
"perda_ori": perda_ori,
|
||||||
|
"freq_pos_baixa": freq_pos_baixa,
|
||||||
|
"freq_ori_baixa": freq_ori_baixa,
|
||||||
|
"orientacao_invalida": ori_invalida,
|
||||||
|
"fix_ruim": fix_ruim_persistente,
|
||||||
|
"rtk_expirado": rtk_falha_persistente,
|
||||||
|
"hacc_ruim": hacc_ruim,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
ContextoGlobalRedis.ModKey(self.t_code),
|
ContextoGlobalRedis.ModKey(self.t_code),
|
||||||
saude=saude_geral
|
saude=saude_geral,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fallback de correcao RTK via NTRIP
|
# Fallback automatico para NTRIP permanece independente do score.
|
||||||
|
# A funcao usa fix/age/hAcc, nao os alertas de frequencia.
|
||||||
if conectado:
|
if conectado:
|
||||||
AGE_HI = 5 # s (ruim)
|
self._atualizar_fallback_ntrip(
|
||||||
AGE_LO = 2 # s (bem estabilizado)
|
fix=fix,
|
||||||
HACC_OK = 40 # cm
|
age=age,
|
||||||
T_BAD = 4 # s p/ ligar NTRIP
|
age_valido=age_valido,
|
||||||
T_GOOD = 10 # s p/ desligar NTRIP
|
hacc=hacc,
|
||||||
MIN_ON = 25 # s mínimo ligado
|
|
||||||
MIN_OFF = 12 # s mínimo desligado
|
|
||||||
|
|
||||||
# ---- NOVOS PARÂMETROS DE RETENTATIVA/BACKOFF ----
|
|
||||||
MAX_TRIES = 3 # nº máx de tentativas falhas de voltar ao LoRa
|
|
||||||
PROBE_EVAL_WINDOW = 30 # s: janela máxima de avaliação ao desligar NTRIP p/ testar LoRa
|
|
||||||
PROBE_BACKOFF = 5 * 60 # s: espera entre testes após esgotar tentativas
|
|
||||||
PROBE_MIN_OFF = max(MIN_OFF, 5) # s: mínimo off durante um "probe" curto
|
|
||||||
|
|
||||||
estado = ContextoGlobalRedis.get_modulo(self.t_code) or {}
|
|
||||||
|
|
||||||
correcao_ntrip = bool(estado.get("correcao_ntrip", False))
|
|
||||||
last_switch_ts = float(estado.get("ntrip_last_switch_ts", 0))
|
|
||||||
bad_since_ts = float(estado.get("ntrip_bad_since_ts", 0))
|
|
||||||
good_since_ts = float(estado.get("ntrip_good_since_ts", 0))
|
|
||||||
|
|
||||||
# ---- NOVOS CAMPOS DE CONTROLE ----
|
|
||||||
probe_attempts = int(estado.get("ntrip_probe_attempts", 0)) # falhas acumuladas
|
|
||||||
probe_start_ts = float(estado.get("ntrip_probe_start_ts", 0)) # quando começou o teste atual (NTRIP desligado)
|
|
||||||
next_probe_ts = float(estado.get("ntrip_next_probe_ts", 0)) # quando pode tentar de novo (backoff)
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
|
|
||||||
# Qualidade atual
|
|
||||||
qual_ruim = (fix < 4) or (age > AGE_HI) or (hAcc >= HACC_OK and hAcc > 0)
|
|
||||||
qual_boa = (fix >= 4) and (age < AGE_LO) and (0 < hAcc < HACC_OK)
|
|
||||||
severa = (fix <= 2) or (age > 15)
|
|
||||||
|
|
||||||
# Atualiza contadores de estabilidade
|
|
||||||
if qual_ruim:
|
|
||||||
bad_since_ts = bad_since_ts or now
|
|
||||||
good_since_ts = 0
|
|
||||||
elif qual_boa:
|
|
||||||
good_since_ts = good_since_ts or now
|
|
||||||
bad_since_ts = 0
|
|
||||||
else:
|
|
||||||
bad_since_ts = 0
|
|
||||||
good_since_ts = 0
|
|
||||||
|
|
||||||
# Cooldowns
|
|
||||||
tempo_desde_troca = now - last_switch_ts
|
|
||||||
pode_ligar = (not correcao_ntrip) and (tempo_desde_troca >= MIN_OFF)
|
|
||||||
pode_deslig = correcao_ntrip and (tempo_desde_troca >= MIN_ON)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# LÓGICA COM TENTATIVAS/BACKOFF
|
|
||||||
# --------------------------------------------------
|
|
||||||
ligar = False
|
|
||||||
desligar = False
|
|
||||||
|
|
||||||
if not correcao_ntrip:
|
|
||||||
# Estamos em LoRa (ou sem correção) → decidir ligar NTRIP
|
|
||||||
if severa:
|
|
||||||
ligar = True
|
|
||||||
elif qual_ruim and bad_since_ts and (now - bad_since_ts >= T_BAD) and pode_ligar:
|
|
||||||
ligar = True
|
|
||||||
|
|
||||||
# Se estamos em um "probe" (testando LoRa) e já passou a janela sem estabilizar, contabiliza falha
|
|
||||||
if probe_start_ts and (now - probe_start_ts >= PROBE_EVAL_WINDOW) and not qual_boa:
|
|
||||||
# falha de tentativa
|
|
||||||
probe_attempts += 1
|
|
||||||
# força ligar NTRIP e inicia backoff
|
|
||||||
ligar = True
|
|
||||||
next_probe_ts = now + PROBE_BACKOFF
|
|
||||||
probe_start_ts = 0 # encerra janela de teste
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Estamos com NTRIP ligado → decidir desligar (testar LoRa)
|
|
||||||
# Regra normal: só desliga se qualidade está boa e estável
|
|
||||||
pronto_pra_desligar = qual_boa and good_since_ts and (now - good_since_ts >= T_GOOD) and pode_deslig
|
|
||||||
|
|
||||||
if pronto_pra_desligar:
|
|
||||||
if probe_attempts >= MAX_TRIES and now < next_probe_ts:
|
|
||||||
# Em modo "persistente NTRIP": já falhou muitas vezes, respeitar backoff (não desliga ainda)
|
|
||||||
desligar = False
|
|
||||||
else:
|
|
||||||
# Podemos testar o LoRa: desliga NTRIP e inicia janela de avaliação ("probe")
|
|
||||||
desligar = True
|
|
||||||
probe_start_ts = now
|
|
||||||
# zera good_since_ts para exigir nova estabilidade sem NTRIP
|
|
||||||
good_since_ts = 0
|
|
||||||
|
|
||||||
# Aplicar decisão
|
|
||||||
if ligar and pode_ligar:
|
|
||||||
correcao_ntrip = True
|
|
||||||
last_switch_ts = now
|
|
||||||
# ao ligar NTRIP, encerra probe (se existia)
|
|
||||||
probe_start_ts = 0
|
|
||||||
|
|
||||||
elif desligar and pode_deslig:
|
|
||||||
correcao_ntrip = False
|
|
||||||
last_switch_ts = now
|
|
||||||
# iniciar "probe": garantir um off mínimo
|
|
||||||
if next_probe_ts and now < next_probe_ts:
|
|
||||||
# segurança: se por algum motivo desligou antes do backoff, reativa NTRIP rápido
|
|
||||||
pass
|
|
||||||
# mantém off por pelo menos PROBE_MIN_OFF (garantido indiretamente via pode_ligar)
|
|
||||||
|
|
||||||
# Se NTRIP está desligado e qualidade ficou realmente estável dentro do probe, considera sucesso
|
|
||||||
if (not correcao_ntrip) and probe_start_ts:
|
|
||||||
# sucesso se já atingiu estabilidade boa por T_GOOD enquanto NTRIP está off
|
|
||||||
if qual_boa and good_since_ts and (now - good_since_ts >= T_GOOD):
|
|
||||||
# sucesso: permanece em LoRa e zera contadores
|
|
||||||
probe_attempts = 0
|
|
||||||
next_probe_ts = 0
|
|
||||||
probe_start_ts = 0
|
|
||||||
|
|
||||||
# Persistir estado
|
|
||||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
|
||||||
ContextoGlobalRedis.ModKey(self.t_code),
|
|
||||||
correcao_ntrip=correcao_ntrip,
|
|
||||||
ntrip_last_switch_ts=last_switch_ts,
|
|
||||||
ntrip_bad_since_ts=bad_since_ts,
|
|
||||||
ntrip_good_since_ts=good_since_ts,
|
|
||||||
ntrip_probe_attempts=probe_attempts,
|
|
||||||
ntrip_probe_start_ts=probe_start_ts,
|
|
||||||
ntrip_next_probe_ts=next_probe_ts
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}")
|
print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}")
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# FALLBACK NTRIP
|
||||||
|
# ==================================================================
|
||||||
|
def _atualizar_fallback_ntrip(self, fix, age, age_valido, hacc):
|
||||||
|
"""Mantem a estrategia existente de radio -> NTRIP com histerese.
|
||||||
|
|
||||||
|
NTRIP nao participa da nota de saude por estar ligado/desligado. Ele e
|
||||||
|
somente uma fonte alternativa quando a qualidade RTK primaria degrada.
|
||||||
|
"""
|
||||||
|
AGE_HI = 5.0
|
||||||
|
AGE_LO = 2.0
|
||||||
|
HACC_OK = 40.0
|
||||||
|
T_BAD = 4.0
|
||||||
|
T_GOOD = 10.0
|
||||||
|
MIN_ON = 25.0
|
||||||
|
MIN_OFF = 12.0
|
||||||
|
|
||||||
|
MAX_TRIES = 3
|
||||||
|
PROBE_EVAL_WINDOW = 30.0
|
||||||
|
PROBE_BACKOFF = 5 * 60.0
|
||||||
|
|
||||||
|
estado = ContextoGlobalRedis.get_modulo(self.t_code) or {}
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
correcao_ntrip = self._bool(estado.get("correcao_ntrip", False))
|
||||||
|
last_switch_ts = self._float(estado.get("ntrip_last_switch_ts", 0.0), 0.0)
|
||||||
|
bad_since_ts = self._float(estado.get("ntrip_bad_since_ts", 0.0), 0.0)
|
||||||
|
good_since_ts = self._float(estado.get("ntrip_good_since_ts", 0.0), 0.0)
|
||||||
|
probe_attempts = self._int(estado.get("ntrip_probe_attempts", 0), 0)
|
||||||
|
probe_start_ts = self._float(estado.get("ntrip_probe_start_ts", 0.0), 0.0)
|
||||||
|
next_probe_ts = self._float(estado.get("ntrip_next_probe_ts", 0.0), 0.0)
|
||||||
|
|
||||||
|
hacc_valido = math.isfinite(hacc) and hacc > 0.0
|
||||||
|
qual_ruim = (
|
||||||
|
fix < 4
|
||||||
|
or not age_valido
|
||||||
|
or age > AGE_HI
|
||||||
|
or (hacc_valido and hacc >= HACC_OK)
|
||||||
|
)
|
||||||
|
qual_boa = (
|
||||||
|
fix in (4, 5)
|
||||||
|
and age_valido
|
||||||
|
and age < AGE_LO
|
||||||
|
and hacc_valido
|
||||||
|
and hacc < HACC_OK
|
||||||
|
)
|
||||||
|
severa = fix <= 2 or (age_valido and age > self.AGE_FAIL_S)
|
||||||
|
|
||||||
|
if qual_ruim:
|
||||||
|
bad_since_ts = bad_since_ts or now
|
||||||
|
good_since_ts = 0.0
|
||||||
|
elif qual_boa:
|
||||||
|
good_since_ts = good_since_ts or now
|
||||||
|
bad_since_ts = 0.0
|
||||||
|
else:
|
||||||
|
bad_since_ts = 0.0
|
||||||
|
good_since_ts = 0.0
|
||||||
|
|
||||||
|
tempo_desde_troca = now - last_switch_ts
|
||||||
|
pode_ligar = not correcao_ntrip and tempo_desde_troca >= MIN_OFF
|
||||||
|
pode_desligar = correcao_ntrip and tempo_desde_troca >= MIN_ON
|
||||||
|
|
||||||
|
ligar = False
|
||||||
|
desligar = False
|
||||||
|
|
||||||
|
if not correcao_ntrip:
|
||||||
|
# Mesmo condicao severa precisa respeitar o cooldown para evitar
|
||||||
|
# liga/desliga rapido e repetitivo.
|
||||||
|
if severa and pode_ligar:
|
||||||
|
ligar = True
|
||||||
|
elif (
|
||||||
|
qual_ruim
|
||||||
|
and bad_since_ts
|
||||||
|
and now - bad_since_ts >= T_BAD
|
||||||
|
and pode_ligar
|
||||||
|
):
|
||||||
|
ligar = True
|
||||||
|
|
||||||
|
# Probe so falha depois da janela completa.
|
||||||
|
if probe_start_ts and now - probe_start_ts >= PROBE_EVAL_WINDOW:
|
||||||
|
if qual_boa and good_since_ts and now - good_since_ts >= T_GOOD:
|
||||||
|
probe_attempts = 0
|
||||||
|
next_probe_ts = 0.0
|
||||||
|
probe_start_ts = 0.0
|
||||||
|
else:
|
||||||
|
probe_attempts += 1
|
||||||
|
next_probe_ts = now + PROBE_BACKOFF
|
||||||
|
probe_start_ts = 0.0
|
||||||
|
if pode_ligar:
|
||||||
|
ligar = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
pronto_probe = (
|
||||||
|
qual_boa
|
||||||
|
and good_since_ts
|
||||||
|
and now - good_since_ts >= T_GOOD
|
||||||
|
and pode_desligar
|
||||||
|
)
|
||||||
|
|
||||||
|
backoff_ativo = probe_attempts >= MAX_TRIES and now < next_probe_ts
|
||||||
|
if pronto_probe and not backoff_ativo:
|
||||||
|
desligar = True
|
||||||
|
|
||||||
|
if ligar:
|
||||||
|
correcao_ntrip = True
|
||||||
|
last_switch_ts = now
|
||||||
|
probe_start_ts = 0.0
|
||||||
|
good_since_ts = 0.0
|
||||||
|
|
||||||
|
elif desligar:
|
||||||
|
correcao_ntrip = False
|
||||||
|
last_switch_ts = now
|
||||||
|
probe_start_ts = now
|
||||||
|
bad_since_ts = 0.0
|
||||||
|
good_since_ts = 0.0
|
||||||
|
|
||||||
|
# Sucesso do probe pode ocorrer antes do fim da janela.
|
||||||
|
if (
|
||||||
|
not correcao_ntrip
|
||||||
|
and probe_start_ts
|
||||||
|
and qual_boa
|
||||||
|
and good_since_ts
|
||||||
|
and now - good_since_ts >= T_GOOD
|
||||||
|
):
|
||||||
|
probe_attempts = 0
|
||||||
|
next_probe_ts = 0.0
|
||||||
|
probe_start_ts = 0.0
|
||||||
|
|
||||||
|
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||||
|
ContextoGlobalRedis.ModKey(self.t_code),
|
||||||
|
correcao_ntrip=correcao_ntrip,
|
||||||
|
ntrip_last_switch_ts=last_switch_ts,
|
||||||
|
ntrip_bad_since_ts=bad_since_ts,
|
||||||
|
ntrip_good_since_ts=good_since_ts,
|
||||||
|
ntrip_probe_attempts=probe_attempts,
|
||||||
|
ntrip_probe_start_ts=probe_start_ts,
|
||||||
|
ntrip_next_probe_ts=next_probe_ts,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# PERSISTENCIA / HISTERese
|
||||||
|
# ==================================================================
|
||||||
|
def _persistente(self, chave, ruim, agora, entrar_s, sair_s):
|
||||||
|
"""Latch temporal independente para cada condicao de health.
|
||||||
|
|
||||||
|
ruim precisa permanecer por entrar_s para ativar. Depois de ativo,
|
||||||
|
precisa permanecer bom por sair_s para limpar.
|
||||||
|
"""
|
||||||
|
estado = self._persist.setdefault(
|
||||||
|
chave,
|
||||||
|
{"ativo": False, "ruim_desde": 0.0, "bom_desde": 0.0},
|
||||||
|
)
|
||||||
|
|
||||||
|
if ruim:
|
||||||
|
estado["bom_desde"] = 0.0
|
||||||
|
if estado["ruim_desde"] <= 0.0:
|
||||||
|
estado["ruim_desde"] = agora
|
||||||
|
|
||||||
|
if (
|
||||||
|
not estado["ativo"]
|
||||||
|
and agora - estado["ruim_desde"] >= entrar_s
|
||||||
|
):
|
||||||
|
estado["ativo"] = True
|
||||||
|
else:
|
||||||
|
estado["ruim_desde"] = 0.0
|
||||||
|
if estado["ativo"]:
|
||||||
|
if estado["bom_desde"] <= 0.0:
|
||||||
|
estado["bom_desde"] = agora
|
||||||
|
if agora - estado["bom_desde"] >= sair_s:
|
||||||
|
estado["ativo"] = False
|
||||||
|
estado["bom_desde"] = 0.0
|
||||||
|
else:
|
||||||
|
estado["bom_desde"] = 0.0
|
||||||
|
|
||||||
|
return estado["ativo"]
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# PENALIDADES
|
||||||
|
# ==================================================================
|
||||||
def _penalidade_fix(self, fix) -> int:
|
def _penalidade_fix(self, fix) -> int:
|
||||||
if fix == 0: return 90 # inválido
|
if fix == 0:
|
||||||
if fix == 1: return 70 # single
|
return 70
|
||||||
if fix == 2: return 40 # DGPS
|
if fix == 1:
|
||||||
if fix == 3: return 30 # PPS
|
return 55
|
||||||
if fix == 4: return 0 # RTK Fixed (melhor caso)
|
if fix == 2:
|
||||||
if fix == 5: return 5 # RTK Float (quase bom)
|
return 30
|
||||||
if fix == 6: return 50 # dead reckoning
|
if fix == 3:
|
||||||
if fix == 7: return 80 # manual
|
return 25
|
||||||
if fix == 8: return 80 # simulador
|
if fix == 4:
|
||||||
return 90
|
return 0
|
||||||
|
if fix == 5:
|
||||||
|
return 5
|
||||||
|
if fix == 6:
|
||||||
|
return 40
|
||||||
|
if fix in (7, 8):
|
||||||
|
return 60
|
||||||
|
return 70
|
||||||
|
|
||||||
def _penalidade_hacc(self, h) -> int:
|
def _penalidade_hacc(self, h) -> int:
|
||||||
if h == 0: return 30
|
if not math.isfinite(h) or h <= 0:
|
||||||
if h < 40: return 0
|
return 15
|
||||||
if h < 80: return 5
|
if h < self.HACC_WARN_CM:
|
||||||
if h < 120: return 10
|
return 0
|
||||||
if h < 200: return 20
|
if h < 80:
|
||||||
|
return 5
|
||||||
|
if h < self.HACC_BAD_CM:
|
||||||
|
return 10
|
||||||
|
if h < 200:
|
||||||
|
return 20
|
||||||
return 30
|
return 30
|
||||||
|
|
||||||
def _penalidade_age(self, age: float) -> float:
|
def _penalidade_age(self, age) -> int:
|
||||||
if age < 0:
|
if not math.isfinite(age) or age < 0:
|
||||||
return 35
|
return 20
|
||||||
return round(min(35, 35 * (1 - math.exp(-age / 8))), 2)
|
if age < self.AGE_WARN_S:
|
||||||
|
return 0
|
||||||
|
if age < 8:
|
||||||
|
return 5
|
||||||
|
if age < 12:
|
||||||
|
return 10
|
||||||
|
if age < self.AGE_FAIL_S:
|
||||||
|
return 20
|
||||||
|
return 30
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# CONVERSOES SEGURAS
|
||||||
|
# ==================================================================
|
||||||
|
@staticmethod
|
||||||
|
def _bool(valor):
|
||||||
|
if isinstance(valor, str):
|
||||||
|
return valor.strip().lower() in ("true", "1", "yes", "sim", "on")
|
||||||
|
return bool(valor)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _float(valor, padrao):
|
||||||
|
try:
|
||||||
|
return float(valor)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return padrao
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _int(valor, padrao):
|
||||||
|
try:
|
||||||
|
return int(valor)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return padrao
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _idade_timestamp(timestamp):
|
||||||
|
"""Calcula idade aceitando timestamp monotonic/perf_counter.
|
||||||
|
|
||||||
|
O codigo existente grava timestamps no mesmo dominio monotonic do
|
||||||
|
processo. Valores ausentes ou futuros sao considerados sem dado.
|
||||||
|
"""
|
||||||
|
if not math.isfinite(timestamp) or timestamp <= 0:
|
||||||
|
return math.inf
|
||||||
|
|
||||||
|
agora = time.monotonic()
|
||||||
|
idade = agora - timestamp
|
||||||
|
if idade < -1.0:
|
||||||
|
return math.inf
|
||||||
|
return max(0.0, idade)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _round_age(valor):
|
||||||
|
return round(valor, 2) if math.isfinite(valor) else None
|
||||||
|
|
|
||||||
|
|
@ -699,8 +699,8 @@ class CameraIMU(ModuloDiagnosticoBase):
|
||||||
limites principais definidos pelo operador.
|
limites principais definidos pelo operador.
|
||||||
|
|
||||||
No contrato físico deste rover:
|
No contrato físico deste rover:
|
||||||
- roll = inclinação frontal
|
- roll = inclinação lateral
|
||||||
- pitch = inclinação lateral
|
- pitch = inclinação frontal
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.roll_attention_deg = (
|
self.roll_attention_deg = (
|
||||||
|
|
@ -777,8 +777,8 @@ class CameraIMU(ModuloDiagnosticoBase):
|
||||||
if mudou:
|
if mudou:
|
||||||
self.mostrar_log(
|
self.mostrar_log(
|
||||||
"Limites angulares atualizados | "
|
"Limites angulares atualizados | "
|
||||||
f"roll/frontal={roll_anterior:.1f}°→{self.roll_max_deg:.1f}° | "
|
f"roll/lateral={roll_anterior:.1f}°→{self.roll_max_deg:.1f}° | "
|
||||||
f"pitch/lateral={pitch_anterior:.1f}°→{self.pitch_max_deg:.1f}° | "
|
f"pitch/frontal={pitch_anterior:.1f}°→{self.pitch_max_deg:.1f}° | "
|
||||||
f"critical=({self.roll_critical_deg:.1f}°, "
|
f"critical=({self.roll_critical_deg:.1f}°, "
|
||||||
f"{self.pitch_critical_deg:.1f}°) | "
|
f"{self.pitch_critical_deg:.1f}°) | "
|
||||||
f"emergency=({self.roll_emergency_deg:.1f}°, "
|
f"emergency=({self.roll_emergency_deg:.1f}°, "
|
||||||
|
|
@ -1322,13 +1322,13 @@ class CameraIMU(ModuloDiagnosticoBase):
|
||||||
"emergency_stop": risco["emergency_stop"],
|
"emergency_stop": risco["emergency_stop"],
|
||||||
|
|
||||||
"convencao_eixos": {
|
"convencao_eixos": {
|
||||||
"roll": "frontal",
|
"roll": "lateral",
|
||||||
"pitch": "lateral",
|
"pitch": "frontal",
|
||||||
},
|
},
|
||||||
|
|
||||||
"limites_angulo": {
|
"limites_angulo": {
|
||||||
"roll": {
|
"roll": {
|
||||||
"semantica": "frontal",
|
"semantica": "lateral",
|
||||||
"attention_deg": round(self.roll_attention_deg, 3),
|
"attention_deg": round(self.roll_attention_deg, 3),
|
||||||
"risk_deg": round(self.roll_risk_deg, 3),
|
"risk_deg": round(self.roll_risk_deg, 3),
|
||||||
"critical_deg": round(self.roll_critical_deg, 3),
|
"critical_deg": round(self.roll_critical_deg, 3),
|
||||||
|
|
@ -1336,7 +1336,7 @@ class CameraIMU(ModuloDiagnosticoBase):
|
||||||
"max_configurado_deg": round(self.roll_max_deg, 3),
|
"max_configurado_deg": round(self.roll_max_deg, 3),
|
||||||
},
|
},
|
||||||
"pitch": {
|
"pitch": {
|
||||||
"semantica": "lateral",
|
"semantica": "frontal",
|
||||||
"attention_deg": round(self.pitch_attention_deg, 3),
|
"attention_deg": round(self.pitch_attention_deg, 3),
|
||||||
"risk_deg": round(self.pitch_risk_deg, 3),
|
"risk_deg": round(self.pitch_risk_deg, 3),
|
||||||
"critical_deg": round(self.pitch_critical_deg, 3),
|
"critical_deg": round(self.pitch_critical_deg, 3),
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,16 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
|
|
||||||
SAUDE_MIN_ALERTA = 80
|
SAUDE_MIN_ALERTA = 80
|
||||||
|
|
||||||
RESPOSTA_RECENTE_MS = 3000.0
|
# Comunicação OID: atraso passageiro não deve parar a operação. A idade da
|
||||||
RESPOSTA_LENTA_MS = 1800.0
|
# própria resposta já representa persistência; por isso a falha só ocorre
|
||||||
|
# no último estágio e depende de haver comando de movimento ativo.
|
||||||
|
RESPOSTA_LENTA_MS = 3000.0
|
||||||
|
RESPOSTA_DEGRADADA_MS = 5000.0
|
||||||
|
RESPOSTA_FALHA_MOVENDO_MS = 8000.0
|
||||||
|
RESPOSTA_FALHA_PARADO_MS = 12000.0
|
||||||
|
RESPOSTA_RECUPERACAO_S = 2.0
|
||||||
|
DESCONECTADO_FALHA_MOVENDO_S = 6.0
|
||||||
|
DESCONECTADO_FALHA_PARADO_S = 10.0
|
||||||
|
|
||||||
COMANDO_GRACE_MS = 1200.0
|
COMANDO_GRACE_MS = 1200.0
|
||||||
COMANDO_ALERTA_MS = 3500.0
|
COMANDO_ALERTA_MS = 3500.0
|
||||||
|
|
@ -64,6 +72,10 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
|
|
||||||
TEMP_EMERGENCIA_CONFIRMADO_S = 1.0
|
TEMP_EMERGENCIA_CONFIRMADO_S = 1.0
|
||||||
TEMP_RECUPERACAO_S = 10.0
|
TEMP_RECUPERACAO_S = 10.0
|
||||||
|
TEMP_AMOSTRAS_MIN_FALHA = 3
|
||||||
|
TEMP_AMOSTRAS_MIN_EMERGENCIA = 3
|
||||||
|
TEMP_AMOSTRA_MAX_IDADE_S = 3.0
|
||||||
|
TEMP_SALTO_SUSPEITO_C = 35.0
|
||||||
|
|
||||||
# Elétricos
|
# Elétricos
|
||||||
TENSAO_FALHA_PERSISTENCIA_S = 2.0
|
TENSAO_FALHA_PERSISTENCIA_S = 2.0
|
||||||
|
|
@ -73,11 +85,21 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
# Freio/comando
|
# Freio/comando
|
||||||
FREIO_RPM_FALHA_PERSISTENCIA_S = 1.0
|
FREIO_RPM_FALHA_PERSISTENCIA_S = 1.0
|
||||||
ALARME_COMANDO_PERSISTENCIA_S = 2.0
|
ALARME_COMANDO_PERSISTENCIA_S = 2.0
|
||||||
|
RECUPERACAO_CONDICAO_S = 2.0
|
||||||
|
|
||||||
|
# Limites extremos permanecem rápidos. São reservados para valores muito
|
||||||
|
# além dos picos vistos em campo, não para pequenas ultrapassagens.
|
||||||
|
TENSAO_EXTREMA_MIN_V = 18.0
|
||||||
|
TENSAO_EXTREMA_MAX_V = 50.0
|
||||||
|
CORRENTE_MOTOR_EXTREMA_A = 90.0
|
||||||
|
CORRENTE_BARRAMENTO_EXTREMA_A = 55.0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.t_code = T_Code.Mov
|
self.t_code = T_Code.Mov
|
||||||
self.nome = "Movimentacao"
|
self.nome = "Movimentacao"
|
||||||
self.timeout = 3
|
# Deve ficar acima do maior timeout tratado por esta classe para que o
|
||||||
|
# diagnóstico específico seja publicado antes do timeout genérico.
|
||||||
|
self.timeout = 15
|
||||||
self.padroes = {}
|
self.padroes = {}
|
||||||
|
|
||||||
self.freq_defaults = {
|
self.freq_defaults = {
|
||||||
|
|
@ -117,6 +139,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
}
|
}
|
||||||
|
|
||||||
self._estados_persistencia = {}
|
self._estados_persistencia = {}
|
||||||
|
self._historico_amostras = {}
|
||||||
|
|
||||||
def atualizar_saude(self):
|
def atualizar_saude(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -162,7 +185,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
saude_individual.append(saude_mod)
|
saude_individual.append(saude_mod)
|
||||||
updates_drivers[f"{endereco_str}.saude"] = saude_mod
|
updates_drivers[f"{endereco_str}.saude"] = saude_mod
|
||||||
|
|
||||||
conectado = self._bool(modulo.get("conectado", False))
|
conectado = self._bool(saude_mod.get("conectado_efetivo", False))
|
||||||
em_uso = self._bool(saude_mod.get("em_uso", True))
|
em_uso = self._bool(saude_mod.get("em_uso", True))
|
||||||
|
|
||||||
if conectado:
|
if conectado:
|
||||||
|
|
@ -244,7 +267,21 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
motivos = []
|
motivos = []
|
||||||
condicoes = []
|
condicoes = []
|
||||||
|
|
||||||
if not conectado:
|
movimento_ativo = self._movimento_ativo(modulo)
|
||||||
|
estado_desconectado = self._estado_persistencia(
|
||||||
|
driver=endereco_str,
|
||||||
|
regra="payload_desconectado",
|
||||||
|
ativa=not conectado,
|
||||||
|
tempo_intermediario_s=2.0,
|
||||||
|
tempo_confirmacao_s=(
|
||||||
|
self.DESCONECTADO_FALHA_MOVENDO_S
|
||||||
|
if movimento_ativo
|
||||||
|
else self.DESCONECTADO_FALHA_PARADO_S
|
||||||
|
),
|
||||||
|
tempo_recuperacao_s=self.RESPOSTA_RECUPERACAO_S,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not conectado and estado_desconectado["nivel"] == "confirmado":
|
||||||
return {
|
return {
|
||||||
"id": endereco_str,
|
"id": endereco_str,
|
||||||
"label": mod_id,
|
"label": mod_id,
|
||||||
|
|
@ -253,15 +290,35 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"motivos": ["Desconectado"],
|
"motivos": ["Desconectado"],
|
||||||
"condicoes_operacionais": [],
|
"condicoes_operacionais": [],
|
||||||
"em_uso": comandar,
|
"em_uso": comandar,
|
||||||
|
"conectado_efetivo": False,
|
||||||
"componentes": self._componentes_zero(),
|
"componentes": self._componentes_zero(),
|
||||||
"parametros": {},
|
"parametros": {},
|
||||||
"debug": self._snapshot_debug(modulo),
|
"debug": self._snapshot_debug(modulo),
|
||||||
}
|
}
|
||||||
|
|
||||||
conectividade = self._avaliar_resposta_recente(modulo, motivos, condicoes)
|
conectado_efetivo = conectado or estado_desconectado["nivel"] in (
|
||||||
comando = self._avaliar_seguimento_comando(modulo, motivos, condicoes)
|
"detectado", "persistente", "recuperando"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not conectado:
|
||||||
|
motivos.append(
|
||||||
|
"Sinal de desconexão transitório; aguardando confirmação "
|
||||||
|
f"({estado_desconectado['duracao_s']:.1f} s)"
|
||||||
|
)
|
||||||
|
condicoes.append({
|
||||||
|
"label": "Conexão OID",
|
||||||
|
"valor": estado_desconectado["duracao_s"],
|
||||||
|
"severidade": 85,
|
||||||
|
"descricao": "Driver publicou desconexão, ainda dentro da tolerância de campo",
|
||||||
|
"acoes": ["Acompanhar a recuperação da comunicação CAN."],
|
||||||
|
})
|
||||||
|
elif estado_desconectado["nivel"] == "recuperando":
|
||||||
|
motivos.append("Comunicação OID recuperada; confirmando estabilidade")
|
||||||
|
|
||||||
|
conectividade = self._avaliar_resposta_recente(endereco_str, modulo, motivos, condicoes)
|
||||||
|
comando = self._avaliar_seguimento_comando(endereco_str, modulo, motivos, condicoes)
|
||||||
valores = self._avaliar_valores_eletricos(endereco_str, modulo, motivos, condicoes)
|
valores = self._avaliar_valores_eletricos(endereco_str, modulo, motivos, condicoes)
|
||||||
freio = self._avaliar_freio(modulo, motivos, condicoes)
|
freio = self._avaliar_freio(endereco_str, modulo, motivos, condicoes)
|
||||||
telemetria = self._avaliar_telemetria(modulo, motivos)
|
telemetria = self._avaliar_telemetria(modulo, motivos)
|
||||||
|
|
||||||
componentes = {
|
componentes = {
|
||||||
|
|
@ -303,7 +360,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
saude = int(max(0, min(100, round(saude))))
|
saude = int(max(0, min(100, round(saude))))
|
||||||
|
|
||||||
status = self._resolver_status_driver(
|
status = self._resolver_status_driver(
|
||||||
conectado=conectado,
|
conectado=conectado_efetivo,
|
||||||
saude=saude,
|
saude=saude,
|
||||||
condicoes_operacionais=condicoes,
|
condicoes_operacionais=condicoes,
|
||||||
)
|
)
|
||||||
|
|
@ -316,6 +373,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"motivos": self._dedupe(motivos),
|
"motivos": self._dedupe(motivos),
|
||||||
"condicoes_operacionais": condicoes,
|
"condicoes_operacionais": condicoes,
|
||||||
"em_uso": comandar,
|
"em_uso": comandar,
|
||||||
|
"conectado_efetivo": conectado_efetivo,
|
||||||
"componentes": componentes,
|
"componentes": componentes,
|
||||||
"parametros": telemetria["parametros"],
|
"parametros": telemetria["parametros"],
|
||||||
"detalhes_valores": valores["detalhes"],
|
"detalhes_valores": valores["detalhes"],
|
||||||
|
|
@ -351,10 +409,16 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
# Componentes
|
# Componentes
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def _avaliar_resposta_recente(self, modulo, motivos, condicoes):
|
def _avaliar_resposta_recente(self, endereco, modulo, motivos, condicoes):
|
||||||
ultima_resposta_ms = self._obter_ultima_resposta_ms(modulo)
|
ultima_resposta_ms = self._obter_ultima_resposta_ms(modulo)
|
||||||
|
movimento_ativo = self._movimento_ativo(modulo)
|
||||||
|
limite_falha_ms = (
|
||||||
|
self.RESPOSTA_FALHA_MOVENDO_MS
|
||||||
|
if movimento_ativo
|
||||||
|
else self.RESPOSTA_FALHA_PARADO_MS
|
||||||
|
)
|
||||||
|
|
||||||
if ultima_resposta_ms > self.RESPOSTA_RECENTE_MS:
|
if ultima_resposta_ms >= limite_falha_ms:
|
||||||
motivos.append(f"Sem resposta recente do OID ({ultima_resposta_ms:.0f} ms)")
|
motivos.append(f"Sem resposta recente do OID ({ultima_resposta_ms:.0f} ms)")
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Resposta OID",
|
"label": "Resposta OID",
|
||||||
|
|
@ -369,13 +433,30 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
})
|
})
|
||||||
return {"score": 0, "sem_resposta": True}
|
return {"score": 0, "sem_resposta": True}
|
||||||
|
|
||||||
|
if ultima_resposta_ms > self.RESPOSTA_DEGRADADA_MS:
|
||||||
|
motivos.append(
|
||||||
|
f"Comunicação OID degradada ({ultima_resposta_ms:.0f} ms); "
|
||||||
|
f"falha em {limite_falha_ms:.0f} ms"
|
||||||
|
)
|
||||||
|
condicoes.append({
|
||||||
|
"label": "Resposta OID",
|
||||||
|
"valor": ultima_resposta_ms,
|
||||||
|
"severidade": 82,
|
||||||
|
"descricao": "Resposta do driver atrasada, ainda sem perda confirmada",
|
||||||
|
"acoes": [
|
||||||
|
"Acompanhar se a comunicação CAN se recupera.",
|
||||||
|
"Verificar ruído, conectores e carga do polling se persistir.",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
return {"score": 55, "sem_resposta": False}
|
||||||
|
|
||||||
if ultima_resposta_ms > self.RESPOSTA_LENTA_MS:
|
if ultima_resposta_ms > self.RESPOSTA_LENTA_MS:
|
||||||
motivos.append(f"Resposta OID lenta ({ultima_resposta_ms:.0f} ms)")
|
motivos.append(f"Resposta OID lenta ({ultima_resposta_ms:.0f} ms)")
|
||||||
return {"score": 75, "sem_resposta": False}
|
return {"score": 78, "sem_resposta": False}
|
||||||
|
|
||||||
return {"score": 100, "sem_resposta": False}
|
return {"score": 100, "sem_resposta": False}
|
||||||
|
|
||||||
def _avaliar_seguimento_comando(self, modulo, motivos, condicoes):
|
def _avaliar_seguimento_comando(self, endereco, modulo, motivos, condicoes):
|
||||||
comandar = self._bool(modulo.get("comandar", True))
|
comandar = self._bool(modulo.get("comandar", True))
|
||||||
rpm = abs(self._float(modulo.get("rpm", 0.0), 0.0))
|
rpm = abs(self._float(modulo.get("rpm", 0.0), 0.0))
|
||||||
rpm_sp = abs(self._float(modulo.get("rpm_sp", 0.0), 0.0))
|
rpm_sp = abs(self._float(modulo.get("rpm_sp", 0.0), 0.0))
|
||||||
|
|
@ -418,7 +499,16 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
# O componente de valores elétricos vai tratar o código.
|
# O componente de valores elétricos vai tratar o código.
|
||||||
return {"score": 70, "falha_seguimento": False}
|
return {"score": 70, "falha_seguimento": False}
|
||||||
|
|
||||||
if alarme_comando:
|
estado_alarme = self._estado_persistencia(
|
||||||
|
driver=endereco,
|
||||||
|
regra="alarme_comando",
|
||||||
|
ativa=alarme_comando,
|
||||||
|
tempo_intermediario_s=0.5,
|
||||||
|
tempo_confirmacao_s=self.ALARME_COMANDO_PERSISTENCIA_S,
|
||||||
|
tempo_recuperacao_s=self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
|
||||||
|
if estado_alarme["nivel"] in ("confirmado", "recuperando"):
|
||||||
motivos.append("Alarme de comando MOV ativo: comando ligado, mas leitura não indica acionamento")
|
motivos.append("Alarme de comando MOV ativo: comando ligado, mas leitura não indica acionamento")
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Comando MOV",
|
"label": "Comando MOV",
|
||||||
|
|
@ -433,6 +523,20 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
})
|
})
|
||||||
return {"score": 20, "falha_seguimento": True}
|
return {"score": 20, "falha_seguimento": True}
|
||||||
|
|
||||||
|
if estado_alarme["ativa"]:
|
||||||
|
motivos.append(
|
||||||
|
"Alarme de comando MOV detectado; aguardando persistência "
|
||||||
|
f"({estado_alarme['duracao_s']:.1f} s)"
|
||||||
|
)
|
||||||
|
condicoes.append({
|
||||||
|
"label": "Comando MOV",
|
||||||
|
"valor": rpm,
|
||||||
|
"severidade": 75,
|
||||||
|
"descricao": "Possível falha de comando ainda não confirmada",
|
||||||
|
"acoes": ["Acompanhar RPM, corrente e próximas atualizações do OID."],
|
||||||
|
})
|
||||||
|
return {"score": 65, "falha_seguimento": False}
|
||||||
|
|
||||||
if freio_sp or freado:
|
if freio_sp or freado:
|
||||||
# Freio é avaliado em _avaliar_freio. Aqui não cobra RPM_SP.
|
# Freio é avaliado em _avaliar_freio. Aqui não cobra RPM_SP.
|
||||||
return {"score": 100, "falha_seguimento": False}
|
return {"score": 100, "falha_seguimento": False}
|
||||||
|
|
@ -573,17 +677,31 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
# Faixas absolutas. Ajustar depois com logs reais.
|
# Faixas absolutas. Ajustar depois com logs reais.
|
||||||
if tensao > 0:
|
if tensao > 0:
|
||||||
if tensao < 25 or tensao > 45:
|
if tensao < 25 or tensao > 45:
|
||||||
motivos.append(f"Tensão MOV em faixa perigosa: {tensao:.1f} V")
|
extrema = tensao < self.TENSAO_EXTREMA_MIN_V or tensao > self.TENSAO_EXTREMA_MAX_V
|
||||||
|
estado = self._estado_persistencia(
|
||||||
|
endereco, "tensao_perigosa", True,
|
||||||
|
0.5, 0.0 if extrema else self.TENSAO_FALHA_PERSISTENCIA_S,
|
||||||
|
self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
confirmada = estado["nivel"] in ("confirmado", "recuperando")
|
||||||
|
motivos.append(
|
||||||
|
f"Tensão MOV em faixa perigosa: {tensao:.1f} V "
|
||||||
|
f"({'confirmada' if confirmada else 'aguardando persistência'})"
|
||||||
|
)
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Tensão MOV",
|
"label": "Tensão MOV",
|
||||||
"valor": tensao,
|
"valor": tensao,
|
||||||
"severidade": 92,
|
"severidade": 92 if confirmada else 75,
|
||||||
"descricao": f"Tensão do driver em faixa perigosa ({tensao:.1f} V)",
|
"descricao": f"Tensão do driver em faixa perigosa ({tensao:.1f} V)",
|
||||||
"acoes": ["Verificar alimentação/bateria/conversor antes de seguir."],
|
"acoes": ["Verificar alimentação/bateria/conversor antes de seguir."],
|
||||||
})
|
})
|
||||||
score -= 70
|
score -= 70 if confirmada else 20
|
||||||
risco_eletrico_termico = True
|
risco_eletrico_termico = risco_eletrico_termico or confirmada
|
||||||
elif tensao < 30 or tensao > 43:
|
elif tensao < 30 or tensao > 43:
|
||||||
|
self._estado_persistencia(
|
||||||
|
endereco, "tensao_perigosa", False, 0.5,
|
||||||
|
self.TENSAO_FALHA_PERSISTENCIA_S, self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
motivos.append(f"Tensão MOV fora da faixa esperada: {tensao:.1f} V")
|
motivos.append(f"Tensão MOV fora da faixa esperada: {tensao:.1f} V")
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Tensão MOV",
|
"label": "Tensão MOV",
|
||||||
|
|
@ -593,22 +711,41 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"acoes": ["Acompanhar alimentação dos drivers de movimento."],
|
"acoes": ["Acompanhar alimentação dos drivers de movimento."],
|
||||||
})
|
})
|
||||||
score -= 20
|
score -= 20
|
||||||
|
else:
|
||||||
|
self._estado_persistencia(
|
||||||
|
endereco, "tensao_perigosa", False, 0.5,
|
||||||
|
self.TENSAO_FALHA_PERSISTENCIA_S, self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
|
||||||
if abs(corrente_motor) > 60:
|
if abs(corrente_motor) > 60:
|
||||||
motivos.append(f"Corrente do motor muito alta: {corrente_motor:.1f} A")
|
extrema = abs(corrente_motor) >= self.CORRENTE_MOTOR_EXTREMA_A
|
||||||
|
estado = self._estado_persistencia(
|
||||||
|
endereco, "corrente_motor_perigosa", True,
|
||||||
|
0.25, 0.0 if extrema else self.CORRENTE_MOTOR_FALHA_PERSISTENCIA_S,
|
||||||
|
self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
confirmada = estado["nivel"] in ("confirmado", "recuperando")
|
||||||
|
motivos.append(
|
||||||
|
f"Corrente do motor muito alta: {corrente_motor:.1f} A "
|
||||||
|
f"({'confirmada' if confirmada else 'pico em confirmação'})"
|
||||||
|
)
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Corrente motor",
|
"label": "Corrente motor",
|
||||||
"valor": corrente_motor,
|
"valor": corrente_motor,
|
||||||
"severidade": 93,
|
"severidade": 93 if confirmada else 78,
|
||||||
"descricao": f"Corrente do motor em faixa perigosa ({corrente_motor:.1f} A)",
|
"descricao": f"Corrente do motor em faixa perigosa ({corrente_motor:.1f} A)",
|
||||||
"acoes": [
|
"acoes": [
|
||||||
"Verificar roda travada, esforço mecânico ou curto.",
|
"Verificar roda travada, esforço mecânico ou curto.",
|
||||||
"Reduzir comando imediatamente se persistir.",
|
"Reduzir comando imediatamente se persistir.",
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
score -= 75
|
score -= 75 if confirmada else 25
|
||||||
risco_eletrico_termico = True
|
risco_eletrico_termico = risco_eletrico_termico or confirmada
|
||||||
elif abs(corrente_motor) > 50:
|
elif abs(corrente_motor) > 50:
|
||||||
|
self._estado_persistencia(
|
||||||
|
endereco, "corrente_motor_perigosa", False, 0.25,
|
||||||
|
self.CORRENTE_MOTOR_FALHA_PERSISTENCIA_S, self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
motivos.append(f"Corrente do motor elevada: {corrente_motor:.1f} A")
|
motivos.append(f"Corrente do motor elevada: {corrente_motor:.1f} A")
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Corrente motor",
|
"label": "Corrente motor",
|
||||||
|
|
@ -621,19 +758,38 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
score -= 30
|
score -= 30
|
||||||
|
else:
|
||||||
|
self._estado_persistencia(
|
||||||
|
endereco, "corrente_motor_perigosa", False, 0.25,
|
||||||
|
self.CORRENTE_MOTOR_FALHA_PERSISTENCIA_S, self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
|
||||||
if abs(corrente_barramento) > 35:
|
if abs(corrente_barramento) > 35:
|
||||||
motivos.append(f"Corrente de barramento muito alta: {corrente_barramento:.1f} A")
|
extrema = abs(corrente_barramento) >= self.CORRENTE_BARRAMENTO_EXTREMA_A
|
||||||
|
estado = self._estado_persistencia(
|
||||||
|
endereco, "corrente_barramento_perigosa", True,
|
||||||
|
0.25, 0.0 if extrema else self.CORRENTE_BARRAMENTO_FALHA_PERSISTENCIA_S,
|
||||||
|
self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
confirmada = estado["nivel"] in ("confirmado", "recuperando")
|
||||||
|
motivos.append(
|
||||||
|
f"Corrente de barramento muito alta: {corrente_barramento:.1f} A "
|
||||||
|
f"({'confirmada' if confirmada else 'pico em confirmação'})"
|
||||||
|
)
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Corrente barramento",
|
"label": "Corrente barramento",
|
||||||
"valor": corrente_barramento,
|
"valor": corrente_barramento,
|
||||||
"severidade": 90,
|
"severidade": 92 if confirmada else 75,
|
||||||
"descricao": f"Corrente de barramento muito alta ({corrente_barramento:.1f} A)",
|
"descricao": f"Corrente de barramento muito alta ({corrente_barramento:.1f} A)",
|
||||||
"acoes": ["Verificar alimentação e esforço dos motores."],
|
"acoes": ["Verificar alimentação e esforço dos motores."],
|
||||||
})
|
})
|
||||||
score -= 55
|
score -= 55 if confirmada else 20
|
||||||
risco_eletrico_termico = True
|
risco_eletrico_termico = risco_eletrico_termico or confirmada
|
||||||
elif abs(corrente_barramento) > 25:
|
elif abs(corrente_barramento) > 25:
|
||||||
|
self._estado_persistencia(
|
||||||
|
endereco, "corrente_barramento_perigosa", False, 0.25,
|
||||||
|
self.CORRENTE_BARRAMENTO_FALHA_PERSISTENCIA_S, self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
motivos.append(f"Corrente de barramento elevada: {corrente_barramento:.1f} A")
|
motivos.append(f"Corrente de barramento elevada: {corrente_barramento:.1f} A")
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Corrente barramento",
|
"label": "Corrente barramento",
|
||||||
|
|
@ -643,6 +799,11 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"acoes": ["Acompanhar consumo e esforço do conjunto."],
|
"acoes": ["Acompanhar consumo e esforço do conjunto."],
|
||||||
})
|
})
|
||||||
score -= 20
|
score -= 20
|
||||||
|
else:
|
||||||
|
self._estado_persistencia(
|
||||||
|
endereco, "corrente_barramento_perigosa", False, 0.25,
|
||||||
|
self.CORRENTE_BARRAMENTO_FALHA_PERSISTENCIA_S, self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
|
||||||
# Temperatura do driver
|
# Temperatura do driver
|
||||||
_penalidade_d, _motivos_d, _condicoes_d, _risco_d = (
|
_penalidade_d, _motivos_d, _condicoes_d, _risco_d = (
|
||||||
|
|
@ -650,6 +811,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
endereco=endereco,
|
endereco=endereco,
|
||||||
temperatura=temperatura_driver,
|
temperatura=temperatura_driver,
|
||||||
regra_desc="driver",
|
regra_desc="driver",
|
||||||
|
modulo=modulo,
|
||||||
|
campo="temperatura_driver",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
motivos.extend(_motivos_d)
|
motivos.extend(_motivos_d)
|
||||||
|
|
@ -661,6 +824,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
endereco=endereco,
|
endereco=endereco,
|
||||||
temperatura=temperatura_motor,
|
temperatura=temperatura_motor,
|
||||||
regra_desc="motor",
|
regra_desc="motor",
|
||||||
|
modulo=modulo,
|
||||||
|
campo="temperatura_motor",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
motivos.extend(_motivos_m)
|
motivos.extend(_motivos_m)
|
||||||
|
|
@ -710,14 +875,23 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"detalhes": detalhes,
|
"detalhes": detalhes,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _avaliar_freio(self, modulo, motivos, condicoes):
|
def _avaliar_freio(self, endereco, modulo, motivos, condicoes):
|
||||||
rpm = abs(self._float(modulo.get("rpm", 0.0), 0.0))
|
rpm = abs(self._float(modulo.get("rpm", 0.0), 0.0))
|
||||||
freado = self._bool(modulo.get("freado", False))
|
freado = self._bool(modulo.get("freado", False))
|
||||||
freio_sp = self._bool(modulo.get("freio_sp", freado))
|
freio_sp = self._bool(modulo.get("freio_sp", freado))
|
||||||
comando_ligado = self._bool(modulo.get("comando_ligado", False))
|
comando_ligado = self._bool(modulo.get("comando_ligado", False))
|
||||||
|
|
||||||
if (freado or freio_sp) and rpm > self.RPM_FREIO_TOL:
|
if (freado or freio_sp) and rpm > self.RPM_FREIO_TOL:
|
||||||
severidade = 92 if rpm > 100 else 75
|
estado = self._estado_persistencia(
|
||||||
|
driver=endereco,
|
||||||
|
regra="freio_com_rpm",
|
||||||
|
ativa=True,
|
||||||
|
tempo_intermediario_s=0.25,
|
||||||
|
tempo_confirmacao_s=self.FREIO_RPM_FALHA_PERSISTENCIA_S,
|
||||||
|
tempo_recuperacao_s=self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
confirmada = estado["nivel"] in ("confirmado", "recuperando")
|
||||||
|
severidade = 92 if confirmada and rpm > 100 else 78 if rpm > 100 else 70
|
||||||
motivos.append(f"Freio ativo com RPM presente ({rpm:.0f})")
|
motivos.append(f"Freio ativo com RPM presente ({rpm:.0f})")
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
"label": "Freio MOV",
|
"label": "Freio MOV",
|
||||||
|
|
@ -735,6 +909,26 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"falha_freio": severidade > 90,
|
"falha_freio": severidade > 90,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
estado_freio = self._estado_persistencia(
|
||||||
|
driver=endereco,
|
||||||
|
regra="freio_com_rpm",
|
||||||
|
ativa=False,
|
||||||
|
tempo_intermediario_s=0.25,
|
||||||
|
tempo_confirmacao_s=self.FREIO_RPM_FALHA_PERSISTENCIA_S,
|
||||||
|
tempo_recuperacao_s=self.RECUPERACAO_CONDICAO_S,
|
||||||
|
)
|
||||||
|
|
||||||
|
if estado_freio["nivel"] == "recuperando":
|
||||||
|
motivos.append("Freio/RPM normalizou; confirmando estabilidade")
|
||||||
|
condicoes.append({
|
||||||
|
"label": "Freio MOV",
|
||||||
|
"valor": rpm,
|
||||||
|
"severidade": 92,
|
||||||
|
"descricao": "Condição de freio com movimento em recuperação",
|
||||||
|
"acoes": ["Aguardar estabilidade antes da liberação automática."],
|
||||||
|
})
|
||||||
|
return {"score": 25, "falha_freio": True}
|
||||||
|
|
||||||
if freado and comando_ligado:
|
if freado and comando_ligado:
|
||||||
motivos.append("Freio ativo enquanto comando de movimento aparece ligado")
|
motivos.append("Freio ativo enquanto comando de movimento aparece ligado")
|
||||||
condicoes.append({
|
condicoes.append({
|
||||||
|
|
@ -794,6 +988,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
endereco,
|
endereco,
|
||||||
temperatura,
|
temperatura,
|
||||||
regra_desc,
|
regra_desc,
|
||||||
|
modulo,
|
||||||
|
campo,
|
||||||
):
|
):
|
||||||
motivos = []
|
motivos = []
|
||||||
condicoes = []
|
condicoes = []
|
||||||
|
|
@ -808,6 +1004,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
|
|
||||||
label = f"Temperatura do {nome_origem}"
|
label = f"Temperatura do {nome_origem}"
|
||||||
|
|
||||||
|
amostra = self._registrar_amostra(endereco, campo, temperatura, modulo)
|
||||||
|
|
||||||
if not self._temperatura_valida(temperatura):
|
if not self._temperatura_valida(temperatura):
|
||||||
motivos.append(
|
motivos.append(
|
||||||
f"Temperatura do {nome_origem} inválida: "
|
f"Temperatura do {nome_origem} inválida: "
|
||||||
|
|
@ -832,6 +1030,32 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
risco_eletrico_termico,
|
risco_eletrico_termico,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not amostra["fresca"]:
|
||||||
|
motivos.append(
|
||||||
|
f"Temperatura do {nome_origem} sem amostra fresca "
|
||||||
|
f"({amostra['idade_s']:.1f} s)"
|
||||||
|
)
|
||||||
|
condicoes.append({
|
||||||
|
"label": label,
|
||||||
|
"valor": temperatura,
|
||||||
|
"severidade": 55,
|
||||||
|
"descricao": "Leitura térmica está congelada ou desatualizada; não confirma sobretemperatura",
|
||||||
|
"acoes": ["Verificar publicação e timestamp do sensor térmico."],
|
||||||
|
})
|
||||||
|
return (0, motivos, condicoes, False)
|
||||||
|
|
||||||
|
if amostra["salto_suspeito"]:
|
||||||
|
motivos.append(
|
||||||
|
f"Salto térmico suspeito no {nome_origem}: {amostra['delta_c']:.1f} °C"
|
||||||
|
)
|
||||||
|
condicoes.append({
|
||||||
|
"label": label,
|
||||||
|
"valor": temperatura,
|
||||||
|
"severidade": 70,
|
||||||
|
"descricao": "Variação térmica instantânea acima do fisicamente plausível; aguardando confirmação",
|
||||||
|
"acoes": ["Verificar sensor, divisor, aterramento e próximas amostras."],
|
||||||
|
})
|
||||||
|
|
||||||
estado_temp_emergencia = self._estado_limite_alto(
|
estado_temp_emergencia = self._estado_limite_alto(
|
||||||
driver=endereco,
|
driver=endereco,
|
||||||
regra=f"temperatura_emergencia_{regra_desc}",
|
regra=f"temperatura_emergencia_{regra_desc}",
|
||||||
|
|
@ -841,6 +1065,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
tempo_intermediario_s=0.0,
|
tempo_intermediario_s=0.0,
|
||||||
tempo_confirmacao_s=self.TEMP_EMERGENCIA_CONFIRMADO_S,
|
tempo_confirmacao_s=self.TEMP_EMERGENCIA_CONFIRMADO_S,
|
||||||
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
|
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
|
||||||
|
amostra_nova=amostra["nova"],
|
||||||
|
min_amostras=self.TEMP_AMOSTRAS_MIN_EMERGENCIA,
|
||||||
)
|
)
|
||||||
|
|
||||||
estado_temp_falha = self._estado_limite_alto(
|
estado_temp_falha = self._estado_limite_alto(
|
||||||
|
|
@ -852,6 +1078,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
tempo_intermediario_s=self.TEMP_FALHA_INTERMEDIARIO_S,
|
tempo_intermediario_s=self.TEMP_FALHA_INTERMEDIARIO_S,
|
||||||
tempo_confirmacao_s=self.TEMP_FALHA_CONFIRMADO_S,
|
tempo_confirmacao_s=self.TEMP_FALHA_CONFIRMADO_S,
|
||||||
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
|
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
|
||||||
|
amostra_nova=amostra["nova"],
|
||||||
|
min_amostras=self.TEMP_AMOSTRAS_MIN_FALHA,
|
||||||
)
|
)
|
||||||
|
|
||||||
estado_temp_alerta = self._estado_limite_alto(
|
estado_temp_alerta = self._estado_limite_alto(
|
||||||
|
|
@ -863,6 +1091,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
tempo_intermediario_s=self.TEMP_ALERTA_INTERMEDIARIO_S,
|
tempo_intermediario_s=self.TEMP_ALERTA_INTERMEDIARIO_S,
|
||||||
tempo_confirmacao_s=self.TEMP_ALERTA_CONFIRMADO_S,
|
tempo_confirmacao_s=self.TEMP_ALERTA_CONFIRMADO_S,
|
||||||
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
|
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
|
||||||
|
amostra_nova=amostra["nova"],
|
||||||
|
min_amostras=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -1331,6 +1561,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
tempo_intermediario_s,
|
tempo_intermediario_s,
|
||||||
tempo_confirmacao_s,
|
tempo_confirmacao_s,
|
||||||
tempo_recuperacao_s=0.0,
|
tempo_recuperacao_s=0.0,
|
||||||
|
amostra_nova=True,
|
||||||
|
min_amostras=1,
|
||||||
):
|
):
|
||||||
agora = time.monotonic()
|
agora = time.monotonic()
|
||||||
chave = (str(driver), str(regra))
|
chave = (str(driver), str(regra))
|
||||||
|
|
@ -1339,6 +1571,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"ativa_desde": None,
|
"ativa_desde": None,
|
||||||
"normal_desde": None,
|
"normal_desde": None,
|
||||||
"confirmada": False,
|
"confirmada": False,
|
||||||
|
"amostras_ativas": 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
if ativa:
|
if ativa:
|
||||||
|
|
@ -1346,10 +1579,17 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
|
|
||||||
if estado["ativa_desde"] is None:
|
if estado["ativa_desde"] is None:
|
||||||
estado["ativa_desde"] = agora
|
estado["ativa_desde"] = agora
|
||||||
|
estado["amostras_ativas"] = 0
|
||||||
|
|
||||||
|
if amostra_nova:
|
||||||
|
estado["amostras_ativas"] += 1
|
||||||
|
|
||||||
duracao_s = max(0.0, agora - estado["ativa_desde"])
|
duracao_s = max(0.0, agora - estado["ativa_desde"])
|
||||||
|
|
||||||
if duracao_s >= tempo_confirmacao_s:
|
if (
|
||||||
|
duracao_s >= tempo_confirmacao_s
|
||||||
|
and estado["amostras_ativas"] >= max(1, int(min_amostras))
|
||||||
|
):
|
||||||
estado["confirmada"] = True
|
estado["confirmada"] = True
|
||||||
|
|
||||||
if estado["confirmada"]:
|
if estado["confirmada"]:
|
||||||
|
|
@ -1364,9 +1604,11 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"nivel": nivel,
|
"nivel": nivel,
|
||||||
"duracao_s": duracao_s,
|
"duracao_s": duracao_s,
|
||||||
"confirmada": estado["confirmada"],
|
"confirmada": estado["confirmada"],
|
||||||
|
"amostras": estado["amostras_ativas"],
|
||||||
}
|
}
|
||||||
|
|
||||||
estado["ativa_desde"] = None
|
estado["ativa_desde"] = None
|
||||||
|
estado["amostras_ativas"] = 0
|
||||||
|
|
||||||
if not estado["confirmada"]:
|
if not estado["confirmada"]:
|
||||||
estado["normal_desde"] = None
|
estado["normal_desde"] = None
|
||||||
|
|
@ -1375,6 +1617,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"nivel": "normal",
|
"nivel": "normal",
|
||||||
"duracao_s": 0.0,
|
"duracao_s": 0.0,
|
||||||
"confirmada": False,
|
"confirmada": False,
|
||||||
|
"amostras": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if estado["normal_desde"] is None:
|
if estado["normal_desde"] is None:
|
||||||
|
|
@ -1391,6 +1634,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"nivel": "normal",
|
"nivel": "normal",
|
||||||
"duracao_s": 0.0,
|
"duracao_s": 0.0,
|
||||||
"confirmada": False,
|
"confirmada": False,
|
||||||
|
"amostras": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1398,6 +1642,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
"nivel": "recuperando",
|
"nivel": "recuperando",
|
||||||
"duracao_s": normal_ha_s,
|
"duracao_s": normal_ha_s,
|
||||||
"confirmada": True,
|
"confirmada": True,
|
||||||
|
"amostras": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _estado_limite_alto(
|
def _estado_limite_alto(
|
||||||
|
|
@ -1410,6 +1655,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
tempo_intermediario_s,
|
tempo_intermediario_s,
|
||||||
tempo_confirmacao_s,
|
tempo_confirmacao_s,
|
||||||
tempo_recuperacao_s,
|
tempo_recuperacao_s,
|
||||||
|
amostra_nova=True,
|
||||||
|
min_amostras=1,
|
||||||
):
|
):
|
||||||
chave = (str(driver), str(regra))
|
chave = (str(driver), str(regra))
|
||||||
persistencia = self._estados_persistencia.get(chave, {})
|
persistencia = self._estados_persistencia.get(chave, {})
|
||||||
|
|
@ -1431,6 +1678,58 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
||||||
tempo_intermediario_s=tempo_intermediario_s,
|
tempo_intermediario_s=tempo_intermediario_s,
|
||||||
tempo_confirmacao_s=tempo_confirmacao_s,
|
tempo_confirmacao_s=tempo_confirmacao_s,
|
||||||
tempo_recuperacao_s=tempo_recuperacao_s,
|
tempo_recuperacao_s=tempo_recuperacao_s,
|
||||||
|
amostra_nova=amostra_nova,
|
||||||
|
min_amostras=min_amostras,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _registrar_amostra(self, endereco, campo, valor, modulo):
|
||||||
|
"""Identifica amostras novas sem confundir polling do health com sensor novo."""
|
||||||
|
agora_mono = time.monotonic()
|
||||||
|
chave = (str(endereco), str(campo))
|
||||||
|
anterior = self._historico_amostras.get(chave)
|
||||||
|
|
||||||
|
ts = 0.0
|
||||||
|
for nome in (
|
||||||
|
f"{campo}_timestamp", f"timestamp_{campo}", f"{campo}_ts",
|
||||||
|
"timestamp",
|
||||||
|
):
|
||||||
|
ts = self._normalizar_timestamp_segundos(modulo.get(nome))
|
||||||
|
if ts > 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
idade_s = max(0.0, time.time() - ts) if ts > 0 else 0.0
|
||||||
|
fresca = ts <= 0 or idade_s <= self.TEMP_AMOSTRA_MAX_IDADE_S
|
||||||
|
valor_f = self._float(valor, 0.0)
|
||||||
|
marcador = (ts, valor_f) if ts > 0 else (None, valor_f)
|
||||||
|
nova = anterior is None or anterior.get("marcador") != marcador
|
||||||
|
|
||||||
|
delta_c = 0.0
|
||||||
|
salto_suspeito = False
|
||||||
|
if nova and anterior is not None:
|
||||||
|
delta_c = abs(valor_f - anterior.get("valor", valor_f))
|
||||||
|
dt = max(0.001, agora_mono - anterior.get("mono", agora_mono))
|
||||||
|
salto_suspeito = delta_c >= self.TEMP_SALTO_SUSPEITO_C and dt <= 5.0
|
||||||
|
|
||||||
|
if nova:
|
||||||
|
self._historico_amostras[chave] = {
|
||||||
|
"marcador": marcador,
|
||||||
|
"valor": valor_f,
|
||||||
|
"mono": agora_mono,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"nova": nova,
|
||||||
|
"fresca": fresca,
|
||||||
|
"idade_s": idade_s,
|
||||||
|
"delta_c": delta_c,
|
||||||
|
"salto_suspeito": salto_suspeito,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _movimento_ativo(self, modulo):
|
||||||
|
rpm_sp = abs(self._float(modulo.get("rpm_sp", 0.0), 0.0))
|
||||||
|
return (
|
||||||
|
self._bool(modulo.get("comando_ligado", rpm_sp > self.RPM_SP_MIN_AVALIAR))
|
||||||
|
or rpm_sp > self.RPM_SP_MIN_AVALIAR
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -911,256 +911,329 @@ class ContextoGlobalRedis:
|
||||||
operacao.get("modo", ModoOperacao.NaoDefinido.value),
|
operacao.get("modo", ModoOperacao.NaoDefinido.value),
|
||||||
ModoOperacao.NaoDefinido,
|
ModoOperacao.NaoDefinido,
|
||||||
)
|
)
|
||||||
|
|
||||||
status_atual = cls._enum_or(
|
status_atual = cls._enum_or(
|
||||||
StatusOperacao,
|
StatusOperacao,
|
||||||
operacao.get("status", StatusOperacao.NaoIniciado.value),
|
operacao.get("status", StatusOperacao.NaoIniciado.value),
|
||||||
StatusOperacao.NaoIniciado,
|
StatusOperacao.NaoIniciado,
|
||||||
)
|
)
|
||||||
|
|
||||||
equipamento_pronto = cls.get_equipamento().get("pronto", False)
|
equipamento_pronto = cls._bool(cls.get_equipamento().get("pronto", False))
|
||||||
|
|
||||||
iniciado = cls._bool(operacao.get("iniciado", False))
|
iniciado = cls._bool(operacao.get("iniciado", False))
|
||||||
configurado = cls._bool(operacao.get("configurado", False))
|
configurado = cls._bool(operacao.get("configurado", False))
|
||||||
status_iniciado_anterior = cls._bool(operacao.get("status_iniciado_anterior", False))
|
status_iniciado_anterior = cls._bool(
|
||||||
|
operacao.get("status_iniciado_anterior", False)
|
||||||
|
)
|
||||||
finalizando = cls._bool(operacao.get("finalizando", False))
|
finalizando = cls._bool(operacao.get("finalizando", False))
|
||||||
calibrando = cls._bool(operacao.get("calibrando", False))
|
calibrando = cls._bool(operacao.get("calibrando", False))
|
||||||
pausa = cls._bool(operacao.get("pausa", False))
|
pausa = cls._bool(operacao.get("pausa", False))
|
||||||
emergencia = cls._bool(operacao.get("emergencia", False))
|
emergencia = cls._bool(operacao.get("emergencia", False))
|
||||||
op_liberada = cls._bool(operacao.get("liberado", False))
|
op_liberada = cls._bool(operacao.get("liberado", False))
|
||||||
|
ja_entrou_em_andamento = cls._bool(
|
||||||
|
operacao.get("ja_entrou_em_andamento", False)
|
||||||
|
)
|
||||||
|
|
||||||
status_anterior_parado = cls._int(
|
status_anterior_parado = cls._int(
|
||||||
operacao.get("status_anterior_parado", StatusOperacao.Parado.value),
|
operacao.get("status_anterior_parado", StatusOperacao.Parado.value),
|
||||||
StatusOperacao.Parado.value,
|
StatusOperacao.Parado.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
updates = {
|
updates_base = {
|
||||||
"status_iniciado_anterior": iniciado,
|
"status_iniciado_anterior": iniciado,
|
||||||
"status_atualizado_em": agora,
|
"status_atualizado_em": agora,
|
||||||
}
|
}
|
||||||
|
|
||||||
if status_iniciado_anterior != iniciado:
|
if status_iniciado_anterior != iniciado:
|
||||||
updates["status_iniciado_alterado_em"] = agora
|
updates_base["status_iniciado_alterado_em"] = agora
|
||||||
|
|
||||||
def _commit(status, motivo, **extras):
|
def _commit(status, motivo, **extras):
|
||||||
updates["status"] = status.value if isinstance(status, StatusOperacao) else int(status)
|
novo_status = (
|
||||||
updates["motivo_status_operacao"] = motivo
|
status.value if isinstance(status, StatusOperacao) else int(status)
|
||||||
|
)
|
||||||
|
updates = dict(updates_base)
|
||||||
updates.update(extras)
|
updates.update(extras)
|
||||||
|
updates["status"] = novo_status
|
||||||
|
updates["motivo_status_operacao"] = motivo
|
||||||
|
|
||||||
|
# Registra somente transições reais, sem gerar log a cada 100 ms.
|
||||||
|
if novo_status != status_atual.value:
|
||||||
|
updates.update({
|
||||||
|
"status_anterior": status_atual.value,
|
||||||
|
"status_alterado_em": agora,
|
||||||
|
"ultima_transicao_status": {
|
||||||
|
"de": status_atual.value,
|
||||||
|
"para": novo_status,
|
||||||
|
"motivo": motivo,
|
||||||
|
"em": agora,
|
||||||
|
"liberado": op_liberada,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
mostrar_log(
|
||||||
|
f"Status operação: {status_atual.name} -> "
|
||||||
|
f"{getattr(status, 'name', novo_status)} | {motivo}"
|
||||||
|
)
|
||||||
|
|
||||||
cls.atualizar_ctx_dict(CtxKey.DadosOperacao, **updates)
|
cls.atualizar_ctx_dict(CtxKey.DadosOperacao, **updates)
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
def _limpar_controle_transitorio():
|
||||||
# 0) Iniciando
|
return {
|
||||||
# ------------------------------------------------------------
|
"bloqueio_pendente_desde": 0.0,
|
||||||
|
"bloqueio_pendente_motivo": "",
|
||||||
|
"retomada_aguardando_desde": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
if not equipamento_pronto:
|
def _parar_imediatamente(motivo):
|
||||||
|
anterior = status_anterior_parado
|
||||||
|
if status_atual != StatusOperacao.Parado:
|
||||||
|
anterior = status_atual.value
|
||||||
_commit(
|
_commit(
|
||||||
StatusOperacao.NaoIniciado,
|
StatusOperacao.Parado,
|
||||||
"Iniciando equipamento",
|
motivo,
|
||||||
|
status_anterior_parado=anterior,
|
||||||
|
ultimo_motivo_parada=motivo,
|
||||||
|
ultima_parada_em=agora,
|
||||||
|
bloqueio_confirmado_em=agora,
|
||||||
|
**_limpar_controle_transitorio(),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# 0) Pré-condições estruturais.
|
||||||
# 1) Não configurada
|
if not equipamento_pronto:
|
||||||
# ------------------------------------------------------------
|
_commit(StatusOperacao.NaoIniciado, "Iniciando equipamento")
|
||||||
|
return
|
||||||
|
|
||||||
if not configurado:
|
if not configurado:
|
||||||
_commit(
|
_commit(StatusOperacao.Parametrizando, "Operação ainda não configurada")
|
||||||
StatusOperacao.Parametrizando,
|
|
||||||
"Operação ainda não configurada",
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# 2) Não iniciada
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
if not iniciado:
|
if not iniciado:
|
||||||
|
extras = {
|
||||||
|
"ja_entrou_em_andamento": False,
|
||||||
|
"inicializacao_operacao_executada": False,
|
||||||
|
"tempo_aguardando": 0.0,
|
||||||
|
**_limpar_controle_transitorio(),
|
||||||
|
}
|
||||||
if status_atual == StatusOperacao.Concluido:
|
if status_atual == StatusOperacao.Concluido:
|
||||||
_commit(
|
_commit(
|
||||||
StatusOperacao.Concluido,
|
StatusOperacao.Concluido,
|
||||||
"Operação concluída e não reiniciada",
|
"Operação concluída e não reiniciada",
|
||||||
|
**extras,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_commit(
|
_commit(
|
||||||
StatusOperacao.Parametrizando,
|
StatusOperacao.Parametrizando,
|
||||||
"Operação configurada, mas ainda não iniciada",
|
"Operação configurada, mas ainda não iniciada",
|
||||||
|
**extras,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# 1) Estados explicitamente comandados não recebem debounce.
|
||||||
# 3) Estados forçados
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
if finalizando:
|
if finalizando:
|
||||||
# Mantém compatibilidade caso não exista StatusOperacao.Finalizando.
|
|
||||||
status_finalizando = cls._status_operacao_por_nome(
|
status_finalizando = cls._status_operacao_por_nome(
|
||||||
"Finalizando",
|
"Finalizando", StatusOperacao.Parado
|
||||||
StatusOperacao.Parado,
|
|
||||||
)
|
|
||||||
|
|
||||||
_commit(
|
|
||||||
status_finalizando,
|
|
||||||
"Operação em finalização",
|
|
||||||
)
|
)
|
||||||
|
_commit(status_finalizando, "Operação em finalização")
|
||||||
return
|
return
|
||||||
|
|
||||||
if calibrando:
|
if calibrando:
|
||||||
_commit(
|
_commit(StatusOperacao.Calibrando, "Operação em calibração")
|
||||||
StatusOperacao.Calibrando,
|
|
||||||
"Operação em calibração",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if pausa:
|
|
||||||
_commit(
|
|
||||||
StatusOperacao.Parado,
|
|
||||||
"Operação pausada pela base",
|
|
||||||
status_anterior_parado=(
|
|
||||||
status_atual.value
|
|
||||||
if status_atual != StatusOperacao.Parado
|
|
||||||
else status_anterior_parado
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if emergencia:
|
if emergencia:
|
||||||
_commit(
|
_parar_imediatamente("Parada de emergência solicitada pela base")
|
||||||
StatusOperacao.Parado,
|
|
||||||
"Parada de emergência solicitada pela base",
|
|
||||||
status_anterior_parado=(
|
|
||||||
status_atual.value
|
|
||||||
if status_atual != StatusOperacao.Parado
|
|
||||||
else status_anterior_parado
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
if pausa:
|
||||||
# 4) Manual
|
_parar_imediatamente("Operação pausada pela base")
|
||||||
# ------------------------------------------------------------
|
return
|
||||||
|
|
||||||
|
# 2) Manual: mantém resposta imediata, como no comportamento original.
|
||||||
if modo == ModoOperacao.Manual:
|
if modo == ModoOperacao.Manual:
|
||||||
if op_liberada:
|
if op_liberada:
|
||||||
_commit(
|
_commit(
|
||||||
StatusOperacao.EmAndamento,
|
StatusOperacao.EmAndamento,
|
||||||
"Operação manual liberada",
|
"Operação manual liberada",
|
||||||
status_anterior_parado=StatusOperacao.Parado.value,
|
status_anterior_parado=StatusOperacao.Parado.value,
|
||||||
|
ja_entrou_em_andamento=True,
|
||||||
|
**_limpar_controle_transitorio(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_commit(
|
_parar_imediatamente(cls._motivo_nao_liberado_status(operacao))
|
||||||
StatusOperacao.Parado,
|
|
||||||
cls._motivo_nao_liberado_status(operacao),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
modos_automaticos = {
|
||||||
# 5) Modos automáticos suportados
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
modos_automaticos = [
|
|
||||||
ModoOperacao.MapaGPS,
|
ModoOperacao.MapaGPS,
|
||||||
ModoOperacao.TreinamentoIA,
|
ModoOperacao.TreinamentoIA,
|
||||||
ModoOperacao.MapeamentoVisual,
|
ModoOperacao.MapeamentoVisual,
|
||||||
ModoOperacao.RetornoBase,
|
ModoOperacao.RetornoBase,
|
||||||
]
|
}
|
||||||
|
|
||||||
if modo not in modos_automaticos:
|
if modo not in modos_automaticos:
|
||||||
_commit(
|
_parar_imediatamente(
|
||||||
StatusOperacao.Parado,
|
f"Modo de operação sem estratégia automática: {modo.name}"
|
||||||
f"Modo de operação sem estratégia automática: {modo.name}",
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Borda de início da operação.
|
# 3) Inicializa uma única vez. Não depende apenas da borda de `iniciado`,
|
||||||
if iniciado and not status_iniciado_anterior:
|
# porque essa borda pode ocorrer enquanto `calibrando=True` e ser perdida.
|
||||||
|
inicializacao_executada = cls._bool(
|
||||||
|
operacao.get("inicializacao_operacao_executada", False)
|
||||||
|
)
|
||||||
|
if not inicializacao_executada:
|
||||||
cls._iniciar_operacao()
|
cls._iniciar_operacao()
|
||||||
updates["tempo_aguardando"] = agora
|
updates_base["tempo_aguardando"] = agora
|
||||||
|
updates_base["ja_entrou_em_andamento"] = False
|
||||||
|
updates_base["inicializacao_operacao_executada"] = True
|
||||||
|
ja_entrou_em_andamento = False
|
||||||
|
|
||||||
# Não liberado.
|
# 4) Bloqueio automático: confirma apenas glitches muito curtos.
|
||||||
|
# As proteções de cada módulo continuam responsáveis por suas próprias
|
||||||
|
# persistências; este filtro de 0,5 s evita um único snapshot incoerente.
|
||||||
if not op_liberada:
|
if not op_liberada:
|
||||||
motivo = cls._motivo_nao_liberado_status(operacao)
|
motivo = cls._motivo_nao_liberado_status(operacao)
|
||||||
|
pendente_desde = cls._float(
|
||||||
novo_status_anterior = status_anterior_parado
|
operacao.get("bloqueio_pendente_desde", 0.0), 0.0
|
||||||
if status_atual != StatusOperacao.Parado:
|
|
||||||
novo_status_anterior = status_atual.value
|
|
||||||
mostrar_log(f"Operação parada: {motivo}")
|
|
||||||
|
|
||||||
_commit(
|
|
||||||
StatusOperacao.Parado,
|
|
||||||
motivo,
|
|
||||||
status_anterior_parado=novo_status_anterior,
|
|
||||||
)
|
)
|
||||||
|
confirmar_s = max(
|
||||||
|
0.0,
|
||||||
|
cls._float(
|
||||||
|
operacao.get("tempo_confirmar_bloqueio_operacao", 0.5), 0.5
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if pendente_desde <= 0:
|
||||||
|
pendente_desde = agora
|
||||||
|
_commit(
|
||||||
|
status_atual,
|
||||||
|
f"Confirmando bloqueio: {motivo}",
|
||||||
|
bloqueio_pendente_desde=agora,
|
||||||
|
bloqueio_pendente_motivo=motivo,
|
||||||
|
bloqueio_pendente_ultimo_motivo=motivo,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
decorrido = agora - pendente_desde
|
||||||
|
if decorrido < confirmar_s:
|
||||||
|
_commit(
|
||||||
|
status_atual,
|
||||||
|
f"Confirmando bloqueio ({decorrido:.1f}/{confirmar_s:.1f}s): {motivo}",
|
||||||
|
bloqueio_pendente_ultimo_motivo=motivo,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_parar_imediatamente(motivo)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Tempo inicial de espera.
|
# Liberação recuperou antes da confirmação: cancela o pulso sem parar.
|
||||||
tempo_aguardando = cls._float(
|
bloqueio_pendente_desde = cls._float(
|
||||||
operacao.get("tempo_aguardando", updates.get("tempo_aguardando", 0.0)),
|
operacao.get("bloqueio_pendente_desde", 0.0), 0.0
|
||||||
0.0,
|
|
||||||
)
|
|
||||||
tempo_aguardar = cls._float(
|
|
||||||
operacao.get("tempo_aguardar_inicio_operacao", 25.0),
|
|
||||||
25.0,
|
|
||||||
)
|
)
|
||||||
|
if bloqueio_pendente_desde > 0:
|
||||||
|
updates_base.update({
|
||||||
|
"ultimo_bloqueio_transitorio_em": agora,
|
||||||
|
"ultimo_bloqueio_transitorio_motivo": str(
|
||||||
|
operacao.get("bloqueio_pendente_motivo", "")
|
||||||
|
),
|
||||||
|
"bloqueio_pendente_desde": 0.0,
|
||||||
|
"bloqueio_pendente_motivo": "",
|
||||||
|
})
|
||||||
|
|
||||||
if tempo_aguardando <= 0:
|
# 5) Espera inicial existe somente antes do primeiro EmAndamento.
|
||||||
tempo_aguardando = agora
|
if not ja_entrou_em_andamento:
|
||||||
updates["tempo_aguardando"] = agora
|
tempo_aguardando = cls._float(
|
||||||
|
operacao.get(
|
||||||
em_espera = (tempo_aguardando + tempo_aguardar) > agora
|
"tempo_aguardando", updates_base.get("tempo_aguardando", 0.0)
|
||||||
|
),
|
||||||
if em_espera:
|
0.0,
|
||||||
restante = max(0.0, (tempo_aguardando + tempo_aguardar) - agora)
|
|
||||||
|
|
||||||
_commit(
|
|
||||||
StatusOperacao.Aguardando,
|
|
||||||
f"Aguardando início da operação: {restante:.1f}s restantes",
|
|
||||||
status_anterior_parado=StatusOperacao.Parado.value,
|
|
||||||
)
|
)
|
||||||
return
|
tempo_aguardar = max(
|
||||||
|
0.0,
|
||||||
|
cls._float(
|
||||||
|
operacao.get("tempo_aguardar_inicio_operacao", 25.0), 25.0
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if tempo_aguardando <= 0:
|
||||||
|
tempo_aguardando = agora
|
||||||
|
updates_base["tempo_aguardando"] = agora
|
||||||
|
|
||||||
# Conclusão da trajetória.
|
restante = (tempo_aguardando + tempo_aguardar) - agora
|
||||||
|
if restante > 0:
|
||||||
|
_commit(
|
||||||
|
StatusOperacao.Aguardando,
|
||||||
|
f"Aguardando início da operação: {restante:.1f}s restantes",
|
||||||
|
status_anterior_parado=StatusOperacao.Parado.value,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 6) Conclusão só é avaliada com a operação liberada.
|
||||||
if cls._trajetoria_concluida():
|
if cls._trajetoria_concluida():
|
||||||
_commit(
|
_commit(
|
||||||
StatusOperacao.Concluido,
|
StatusOperacao.Concluido,
|
||||||
"Trajetória concluída",
|
"Trajetória concluída",
|
||||||
status_anterior_parado=StatusOperacao.Parado.value,
|
status_anterior_parado=StatusOperacao.Parado.value,
|
||||||
|
**_limpar_controle_transitorio(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if iniciado and not finalizando:
|
if iniciado and not finalizando:
|
||||||
cls._finalizar_operacao()
|
cls._finalizar_operacao()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Recuperou de Parado.
|
# 7) Retomada usa uma janela própria e curta; nunca reinicia a partida.
|
||||||
if status_atual == StatusOperacao.Parado:
|
if status_atual == StatusOperacao.Parado and ja_entrou_em_andamento:
|
||||||
_commit(
|
retomada_desde = cls._float(
|
||||||
StatusOperacao.Aguardando,
|
operacao.get("retomada_aguardando_desde", 0.0), 0.0
|
||||||
"Operação liberada novamente. Reiniciando janela de segurança",
|
|
||||||
tempo_aguardando=agora,
|
|
||||||
status_anterior_parado=StatusOperacao.Parado.value,
|
|
||||||
)
|
)
|
||||||
return
|
tempo_retomada = max(
|
||||||
|
0.0,
|
||||||
|
cls._float(operacao.get("tempo_aguardar_retomada", 1.0), 1.0),
|
||||||
|
)
|
||||||
|
if retomada_desde <= 0:
|
||||||
|
retomada_desde = agora
|
||||||
|
|
||||||
# Restaura status anterior à parada somente se fizer sentido.
|
restante = (retomada_desde + tempo_retomada) - agora
|
||||||
status_anterior_enum = cls._enum_or(
|
if restante > 0:
|
||||||
StatusOperacao,
|
_commit(
|
||||||
status_anterior_parado,
|
StatusOperacao.Aguardando,
|
||||||
StatusOperacao.Parado,
|
f"Bloqueio removido; retomada segura em {restante:.1f}s",
|
||||||
|
retomada_aguardando_desde=retomada_desde,
|
||||||
|
status_anterior_parado=StatusOperacao.EmAndamento.value,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Se o ciclo anterior já publicou Aguardando de retomada, respeita o prazo.
|
||||||
|
retomada_desde = cls._float(
|
||||||
|
operacao.get("retomada_aguardando_desde", 0.0), 0.0
|
||||||
)
|
)
|
||||||
|
if retomada_desde > 0 and ja_entrou_em_andamento:
|
||||||
if status_anterior_enum in [StatusOperacao.Aguardando, StatusOperacao.EmAndamento]:
|
tempo_retomada = max(
|
||||||
_commit(
|
0.0,
|
||||||
status_anterior_enum,
|
cls._float(operacao.get("tempo_aguardar_retomada", 1.0), 1.0),
|
||||||
"Restaurando status anterior à parada",
|
|
||||||
status_anterior_parado=StatusOperacao.Parado.value,
|
|
||||||
)
|
)
|
||||||
return
|
restante = (retomada_desde + tempo_retomada) - agora
|
||||||
|
if restante > 0:
|
||||||
|
_commit(
|
||||||
|
StatusOperacao.Aguardando,
|
||||||
|
f"Bloqueio removido; retomada segura em {restante:.1f}s",
|
||||||
|
status_anterior_parado=StatusOperacao.EmAndamento.value,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Normal.
|
|
||||||
|
# 8) Estado normal. A marca evita que uma recuperação futura repita os 25 s.
|
||||||
|
motivo = (
|
||||||
|
"Operação retomada com segurança"
|
||||||
|
if status_atual in (StatusOperacao.Parado, StatusOperacao.Aguardando)
|
||||||
|
and ja_entrou_em_andamento
|
||||||
|
else "Operação em andamento"
|
||||||
|
)
|
||||||
_commit(
|
_commit(
|
||||||
StatusOperacao.EmAndamento,
|
StatusOperacao.EmAndamento,
|
||||||
"Operação em andamento",
|
motivo,
|
||||||
status_anterior_parado=StatusOperacao.Parado.value,
|
status_anterior_parado=StatusOperacao.Parado.value,
|
||||||
|
ja_entrou_em_andamento=True,
|
||||||
|
bloqueio_pendente_desde=0.0,
|
||||||
|
bloqueio_pendente_motivo="",
|
||||||
|
retomada_aguardando_desde=0.0,
|
||||||
|
ultima_retomada_em=(
|
||||||
|
agora
|
||||||
|
if status_atual in (StatusOperacao.Parado, StatusOperacao.Aguardando)
|
||||||
|
else cls._float(operacao.get("ultima_retomada_em", 0.0), 0.0)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue