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}");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var op = Variaveis.OperacaoEmAndamento;
|
||||
|
|
|
|||
|
|
@ -454,8 +454,8 @@ namespace AgroBase.Models
|
|||
MapaFeatureModel feature = mapaService.DadosMapa.features
|
||||
.FirstOrDefault(x =>
|
||||
x != null &&
|
||||
x.geometry != null &&
|
||||
string.Equals(x.geometry.id, idSelecionado, StringComparison.Ordinal)
|
||||
x.properties != null &&
|
||||
string.Equals(x.properties?.Id?.Trim(), idSelecionado?.Trim(), StringComparison.Ordinal)
|
||||
);
|
||||
|
||||
if (feature?.geometry?.coordinates == null || feature.geometry.coordinates.Count < 2)
|
||||
|
|
|
|||
|
|
@ -1363,10 +1363,15 @@ namespace AgroBase.Models
|
|||
await Task.Delay(2000);
|
||||
|
||||
op.Sensoriamento.Operacao.OperacaoIniciada = false;
|
||||
op.DefinirEstadoCalibragemRuntime(false);
|
||||
op.DefinirEmergencia(false);
|
||||
op.Sensoriamento.Operacao.Calibrando = 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;
|
||||
|
||||
|
|
@ -1732,7 +1737,10 @@ namespace AgroBase.Models
|
|||
{
|
||||
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.");
|
||||
}
|
||||
|
|
@ -1751,11 +1759,73 @@ namespace AgroBase.Models
|
|||
}
|
||||
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 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 _timeoutSensor_s;
|
||||
|
|
@ -81,8 +86,8 @@ namespace AgroBase.Services
|
|||
double tensao_nominal_V = 36.0,
|
||||
double daly_corrente_descarga_sinal = -1.0,
|
||||
double sensor_corrente_descarga_sinal = 1.0,
|
||||
double timeout_bms_s = 3.0,
|
||||
double timeout_sensor_s = 3.0)
|
||||
double timeout_bms_s = 6.0,
|
||||
double timeout_sensor_s = 4.0)
|
||||
{
|
||||
CapacidadeMaxima_Ah = Math.Max(0, max_Ah);
|
||||
CargaMaxima_C = CapacidadeMaxima_Ah * 3600.0;
|
||||
|
|
@ -140,6 +145,9 @@ namespace AgroBase.Services
|
|||
private string _ultimaFonteIntegrada = "";
|
||||
private string _ultimaFonteLogada = "";
|
||||
|
||||
private double _tensaoAnteriorIntegracao_V;
|
||||
private double _correnteAnteriorIntegracao_A;
|
||||
|
||||
public double CapacidadeMaxima_Ah { get; private set; }
|
||||
private double CargaMaxima_C { get; set; }
|
||||
|
||||
|
|
@ -507,6 +515,18 @@ namespace AgroBase.Services
|
|||
private double _somaEnergiaJanela_Wh;
|
||||
|
||||
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; }
|
||||
|
||||
|
|
@ -607,8 +627,14 @@ namespace AgroBase.Services
|
|||
|
||||
AplicarFonteAtiva(ativa);
|
||||
|
||||
if (_estadoInicializado)
|
||||
{
|
||||
AcumularDistanciaPendente(distanciaTotalMetrosAtual);
|
||||
}
|
||||
|
||||
if (!Iniciado || ativa == null)
|
||||
{
|
||||
InvalidarIntervaloAutonomia("fonte_indisponivel");
|
||||
MarcarAutonomiaIndisponivel();
|
||||
return;
|
||||
}
|
||||
|
|
@ -617,6 +643,7 @@ namespace AgroBase.Services
|
|||
|
||||
if (!leituraAtual.HasValue || leituraAtual.Value == DateTime.MinValue)
|
||||
{
|
||||
InvalidarIntervaloAutonomia("timestamp_invalido");
|
||||
MarcarAutonomiaIndisponivel();
|
||||
return;
|
||||
}
|
||||
|
|
@ -637,16 +664,13 @@ namespace AgroBase.Services
|
|||
* Ao trocar de Daly para AB36V, ou vice-versa, não integra
|
||||
* o intervalo entre relógios/fontes diferentes.
|
||||
*/
|
||||
if (!string.Equals(_ultimaFonteIntegrada,FonteAtiva, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(_ultimaFonteIntegrada, FonteAtiva, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_ultimaFonteIntegrada = FonteAtiva;
|
||||
|
||||
_ultimaLeituraIntegrada = leituraAtual.Value;
|
||||
|
||||
_dt = 0;
|
||||
DescartarIntervaloPendente("troca_fonte");
|
||||
PrepararBaselineEletrico(ativa, leituraAtual.Value);
|
||||
|
||||
AtualizarEstadoEnergia();
|
||||
AtualizarAutonomiaSemIntegrar(distanciaTotalMetrosAtual);
|
||||
RecalcularAutonomia();
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -656,26 +680,32 @@ namespace AgroBase.Services
|
|||
_dt = 0;
|
||||
|
||||
AtualizarEstadoEnergia();
|
||||
AtualizarAutonomiaSemIntegrar(distanciaTotalMetrosAtual);
|
||||
RecalcularAutonomia();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_dt = (leituraAtual.Value - _ultimaLeituraIntegrada.Value).TotalSeconds;
|
||||
|
||||
_ultimaLeituraIntegrada = leituraAtual.Value;
|
||||
|
||||
if (_dt <= 0 || _dt > DtMaximoIntegracao_s)
|
||||
{
|
||||
Variaveis.MostrarLog($"[COULOMB COUNTER] dt inválido: " + $"{_dt:F3}s | fonte={FonteAtiva}");
|
||||
|
||||
DescartarIntervaloPendente("dt_invalido");
|
||||
PrepararBaselineEletrico(ativa, leituraAtual.Value);
|
||||
|
||||
AtualizarEstadoEnergia();
|
||||
AtualizarAutonomiaSemIntegrar(distanciaTotalMetrosAtual);
|
||||
RecalcularAutonomia();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
IntegrarEnergiaECarga(_dt);
|
||||
double deltaEnergiaDescarga_Wh =
|
||||
IntegrarEnergiaECarga(_dt, ativa);
|
||||
|
||||
_ultimaLeituraIntegrada = leituraAtual.Value;
|
||||
_tensaoAnteriorIntegracao_V = ativa.Tensao_V;
|
||||
_correnteAnteriorIntegracao_A = ativa.CorrenteLiquida_A;
|
||||
|
||||
AtualizarEstadoEnergia();
|
||||
|
||||
|
|
@ -683,7 +713,7 @@ namespace AgroBase.Services
|
|||
|
||||
AtualizarTempoRestante();
|
||||
|
||||
AtualizarAutonomia(distanciaTotalMetrosAtual, _dt);
|
||||
FecharIntervaloAutonomia(_dt, deltaEnergiaDescarga_Wh);
|
||||
|
||||
AtualizarDiferencasSoc();
|
||||
}
|
||||
|
|
@ -937,6 +967,9 @@ namespace AgroBase.Services
|
|||
|
||||
_ultimaFonteIntegrada = FonteAtiva;
|
||||
|
||||
_tensaoAnteriorIntegracao_V = TensaoInstantanea;
|
||||
_correnteAnteriorIntegracao_A = CorrenteLiquida_A;
|
||||
|
||||
_estadoInicializado = true;
|
||||
|
||||
_dt = 0;
|
||||
|
|
@ -945,12 +978,29 @@ namespace AgroBase.Services
|
|||
|
||||
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.
|
||||
|
|
@ -958,9 +1008,9 @@ namespace AgroBase.Services
|
|||
*/
|
||||
_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);
|
||||
|
||||
|
|
@ -969,14 +1019,16 @@ namespace AgroBase.Services
|
|||
_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);
|
||||
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);
|
||||
|
||||
|
|
@ -985,10 +1037,13 @@ namespace AgroBase.Services
|
|||
_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);
|
||||
}
|
||||
|
||||
return deltaEnergiaDescarga_Wh;
|
||||
}
|
||||
|
||||
private void AtualizarEstadoEnergia()
|
||||
|
|
@ -1051,22 +1106,73 @@ namespace AgroBase.Services
|
|||
// Autonomia por distância
|
||||
// ============================================================
|
||||
|
||||
private void AtualizarAutonomia(double distanciaTotalMetrosAtual, double deltaTempo_s)
|
||||
private void AcumularDistanciaPendente(double 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();
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -1314,6 +1420,12 @@ namespace AgroBase.Services
|
|||
_somaEnergiaJanela_Wh = 0;
|
||||
|
||||
_distanciaTotalMetrosAnterior = distanciaAtual_m;
|
||||
_distanciaPendenteAutonomia_m = 0;
|
||||
_intervaloAutonomiaConfiavel = true;
|
||||
_aguardandoBaselineAutonomia = true;
|
||||
|
||||
DistanciaDescartadaAutonomia_m = 0;
|
||||
EstadoIntervaloAutonomia = "aguardando_baseline";
|
||||
|
||||
WhPorMetroJanela = 0;
|
||||
WhPorMetroHistorico = 0;
|
||||
|
|
@ -1349,6 +1461,9 @@ namespace AgroBase.Services
|
|||
_ultimaFonteIntegrada = "";
|
||||
_ultimaFonteLogada = "";
|
||||
|
||||
_tensaoAnteriorIntegracao_V = 0;
|
||||
_correnteAnteriorIntegracao_A = 0;
|
||||
|
||||
_cargaIntegradaAtual_C = 0;
|
||||
CargaAtual_C = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ class CameraMultispectral:
|
|||
nome_sensor=f"{self.modelo}_imu",
|
||||
modo=self.imu_modo,
|
||||
gyro_unidade="deg_s",
|
||||
referencial_robo="r,p,y",
|
||||
referencial_robo="-r,p,-y",
|
||||
)
|
||||
else:
|
||||
self.tem_imu = False
|
||||
|
|
@ -752,12 +752,30 @@ class CameraMultispectral:
|
|||
if self.imu is not None:
|
||||
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 = []
|
||||
saude = 50
|
||||
|
||||
status = self._safe_get_status()
|
||||
running = bool(status.get("running", False))
|
||||
|
||||
conectado = bool(
|
||||
self.iniciado
|
||||
and self.client is not None
|
||||
|
|
@ -765,6 +783,10 @@ class CameraMultispectral:
|
|||
and not self._falha_fatal_pendente
|
||||
)
|
||||
|
||||
# Falha realmente crítica de hardware/pipeline.
|
||||
# Estas NÃO recebem tolerância.
|
||||
falha_critica_imediata = False
|
||||
|
||||
performance = {
|
||||
"temperatura": 0.0,
|
||||
"memoria_usada": 0.0,
|
||||
|
|
@ -774,12 +796,22 @@ class CameraMultispectral:
|
|||
"async_capture": status.get("async_capture") or {},
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# PIPELINE IA
|
||||
# ============================================================
|
||||
|
||||
if pipeline_ia is not None:
|
||||
self._ultimo_pipeline_ia = dict(pipeline_ia)
|
||||
|
||||
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
|
||||
|
||||
# ============================================================
|
||||
# IMU
|
||||
# ============================================================
|
||||
|
||||
imu = self.imu.get_dados() if self.imu else {
|
||||
"valido": False,
|
||||
"calibrado": False,
|
||||
|
|
@ -800,68 +832,118 @@ class CameraMultispectral:
|
|||
|
||||
resultado = dict(self._ultimo_resultado_tensor)
|
||||
|
||||
# ============================================================
|
||||
# MÉTRICAS DO DEVICE
|
||||
# ============================================================
|
||||
|
||||
try:
|
||||
metrics = (
|
||||
self.client.get_device_metrics()
|
||||
if self.client is not None
|
||||
else {"ok": False, "error": "client indisponível"}
|
||||
else {
|
||||
"ok": False,
|
||||
"error": "client indisponível",
|
||||
}
|
||||
)
|
||||
|
||||
performance["device_metrics"] = metrics
|
||||
|
||||
temp_raw = metrics.get("temperature_average_c")
|
||||
|
||||
if temp_raw is None:
|
||||
performance["temperatura"] = None
|
||||
motivos.append("Temperatura interna indisponível")
|
||||
saude -= 10
|
||||
|
||||
else:
|
||||
temp = float(temp_raw)
|
||||
performance["temperatura"] = temp
|
||||
|
||||
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)
|
||||
falha_critica_imediata = True
|
||||
|
||||
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)
|
||||
|
||||
elif temp >= 85:
|
||||
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
|
||||
motivos.append(
|
||||
f"Temperatura elevada: {temp:.1f} °C"
|
||||
)
|
||||
saude -= 15
|
||||
|
||||
elif temp >= 75:
|
||||
motivos.append(f"Aquecimento preventivo: {temp:.1f} °C")
|
||||
motivos.append(
|
||||
f"Aquecimento preventivo: {temp:.1f} °C"
|
||||
)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
speed = str(metrics.get("usb_speed", "") or "")
|
||||
performance["velocidade"] = speed
|
||||
|
||||
if speed and speed.lower() not in ("super", "superplus"):
|
||||
motivos.append(f"USB lenta: {speed}")
|
||||
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
|
||||
|
||||
if running and async_status:
|
||||
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
|
||||
|
||||
erro_async = async_status.get("last_error")
|
||||
|
||||
if erro_async:
|
||||
motivos.append(f"Captura assíncrona: {erro_async}")
|
||||
motivos.append(
|
||||
f"Captura assíncrona: {erro_async}"
|
||||
)
|
||||
|
||||
if not running:
|
||||
motivos.append("Pipeline parada")
|
||||
saude -= 20
|
||||
falha_critica_imediata = True
|
||||
saude = 0
|
||||
|
||||
except Exception as e:
|
||||
resultado = {
|
||||
|
|
@ -869,66 +951,243 @@ class CameraMultispectral:
|
|||
"duracao": 0.0,
|
||||
"frame_valido": False,
|
||||
}
|
||||
|
||||
if self._is_erro_fatal_depthai(e):
|
||||
conectado = False
|
||||
falha_critica_imediata = True
|
||||
|
||||
# ============================================================
|
||||
# TENSOR
|
||||
# ============================================================
|
||||
|
||||
agora = time.time()
|
||||
agora_mono = time.monotonic()
|
||||
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
motivos.append("desconectado")
|
||||
saude = 0
|
||||
falha_critica_imediata = True
|
||||
|
||||
elif resultado.get("erro"):
|
||||
motivos.append(resultado["erro"])
|
||||
saude = 0
|
||||
falha_critica_imediata = True
|
||||
|
||||
elif not resultado.get("frame_valido", False):
|
||||
motivos.append("Tensor inválido ou indisponível")
|
||||
saude = 0
|
||||
falha_critica_imediata = True
|
||||
|
||||
else:
|
||||
saude += 50
|
||||
|
||||
if not frame_recente:
|
||||
motivos.append(f"Tensor sem atualização há {agora - ts_tensor:.2f}s")
|
||||
saude = 0
|
||||
if tensor_degradado:
|
||||
motivos.append(
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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 []:
|
||||
texto = f"Pipeline IA: {motivo}"
|
||||
|
||||
if texto not in motivos:
|
||||
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)
|
||||
|
||||
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:
|
||||
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
|
||||
elif saude < 80:
|
||||
|
||||
elif (
|
||||
tensor_degradado
|
||||
or pipeline_degradado
|
||||
or saude < 80
|
||||
):
|
||||
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 = {
|
||||
"timestamp": agora,
|
||||
|
|
@ -938,6 +1197,7 @@ class CameraMultispectral:
|
|||
"motivos": motivos,
|
||||
"saude_individual": [],
|
||||
}
|
||||
|
||||
self.ultima_saude = saude_geral
|
||||
|
||||
from camera_worker.manager import definir_saude_camera
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ def main():
|
|||
T_Code.Sen: ModuloSensoriamento(),
|
||||
T_Code.Atu: ModuloAtuador(),
|
||||
#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.Lvx: ModuloLivox(),
|
||||
T_Code.Ipb: ModuloIPBribge(),
|
||||
|
|
|
|||
|
|
@ -1,356 +1,628 @@
|
|||
import math
|
||||
import time
|
||||
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
self.t_code = T_Code.Gps
|
||||
self.nome = "UM982"
|
||||
self.timeout = 5
|
||||
|
||||
self._rtk_expirado_desde = 0.0
|
||||
self._rtk_recuperado_desde = 0.0
|
||||
self._rtk_falha_persistente = False
|
||||
# Cada condicao possui latch proprio. Assim um unico frame ruim nao
|
||||
# cria um alerta e um unico frame bom tambem nao mascara uma falha.
|
||||
self._persist = {}
|
||||
|
||||
# ==================================================================
|
||||
# API PRINCIPAL
|
||||
# ==================================================================
|
||||
def atualizar_saude(self):
|
||||
try:
|
||||
now = time.perf_counter()
|
||||
agora_mono = time.monotonic()
|
||||
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)
|
||||
freq_pos = modulo.get("freq", {}).get("posicao", 0.0)
|
||||
freq_ori = modulo.get("freq", {}).get("orientacao", 0.0)
|
||||
ts_pos = modulo.get("timestamp", {}).get("posicao", 0.0)
|
||||
ts_ori = modulo.get("timestamp", {}).get("orientacao", 0.0)
|
||||
fail_pos = (now - ts_pos) > 2.0
|
||||
fail_ori = (now - ts_ori) > 2.0
|
||||
fix = modulo.get("fix", 0)
|
||||
rtk = modulo.get("rtk", False)
|
||||
hAcc = modulo.get("hAcc", 0)
|
||||
age_bruto = modulo.get("age", -1)
|
||||
ntrip = modulo.get("correcao_ntrip", False)
|
||||
conectado = self._bool(modulo.get("conectado", False))
|
||||
freq_base = self._float(modulo.get("freq_base", 1.0), 1.0)
|
||||
if not math.isfinite(freq_base) or freq_base <= 0:
|
||||
freq_base = 1.0
|
||||
|
||||
freq = modulo.get("freq") or {}
|
||||
timestamps = modulo.get("timestamp") or {}
|
||||
|
||||
freq_pos = max(0.0, self._float(freq.get("posicao", 0.0), 0.0))
|
||||
freq_ori = max(0.0, self._float(freq.get("orientacao", 0.0), 0.0))
|
||||
ts_pos = self._float(timestamps.get("posicao", 0.0), 0.0)
|
||||
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 {}
|
||||
status_ori = str(orientacao.get("status", "")).strip().upper()
|
||||
valor_ori_bruto = orientacao.get("valor", 9999.0)
|
||||
valida_redis = orientacao.get("valida", False)
|
||||
valor_ori = self._float(orientacao.get("valor", math.nan), math.nan)
|
||||
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)
|
||||
except (TypeError, ValueError):
|
||||
age = -1.0
|
||||
# ----------------------------------------------------------
|
||||
# Persistencias / latches
|
||||
# ----------------------------------------------------------
|
||||
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
|
||||
AGE_FAIL = 15.0
|
||||
AGE_RECOVER = 3.0
|
||||
T_FAIL = 2.0
|
||||
T_RECOVER = 3.0
|
||||
agora_mono = time.monotonic()
|
||||
age_valido = math.isfinite(age) and age >= 0.0
|
||||
correcao_expirada = not age_valido or age >= AGE_FAIL
|
||||
correcao_recuperada = (age_valido and age <= AGE_RECOVER and bool(rtk))
|
||||
if correcao_expirada:
|
||||
if self._rtk_expirado_desde <= 0:
|
||||
self._rtk_expirado_desde = agora_mono
|
||||
self._rtk_recuperado_desde = 0.0
|
||||
if (agora_mono - self._rtk_expirado_desde >= T_FAIL):
|
||||
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
|
||||
freq_min_alerta = freq_base * self.RX_WARN_RATIO
|
||||
freq_pos_baixa = self._persistente(
|
||||
"freq_pos_baixa",
|
||||
ruim=(idade_pos <= self.RX_FAIL_AGE_S and freq_pos < freq_min_alerta),
|
||||
agora=agora_mono,
|
||||
entrar_s=self.FREQ_WARN_PERSIST_S,
|
||||
sair_s=self.FREQ_RECOVER_S,
|
||||
)
|
||||
freq_ori_baixa = self._persistente(
|
||||
"freq_ori_baixa",
|
||||
ruim=(idade_ori <= self.RX_FAIL_AGE_S and freq_ori < freq_min_alerta),
|
||||
agora=agora_mono,
|
||||
entrar_s=self.FREQ_WARN_PERSIST_S,
|
||||
sair_s=self.FREQ_RECOVER_S,
|
||||
)
|
||||
|
||||
# Proteção caso o Redis devolva booleano como texto.
|
||||
if isinstance(valida_redis, str):
|
||||
valida_redis = valida_redis.strip().lower() in (
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
"sim",
|
||||
)
|
||||
else:
|
||||
valida_redis = bool(valida_redis)
|
||||
ori_invalida_agora = not (
|
||||
idade_ori <= self.RX_FAIL_AGE_S
|
||||
and valida_ori
|
||||
and status_ori == "A"
|
||||
and valor_ori_valido
|
||||
)
|
||||
ori_invalida = self._persistente(
|
||||
"ori_invalida",
|
||||
ruim=ori_invalida_agora and not perda_ori,
|
||||
agora=agora_mono,
|
||||
entrar_s=self.ORI_INVALID_PERSIST_S,
|
||||
sair_s=self.ORI_RECOVER_S,
|
||||
)
|
||||
|
||||
try:
|
||||
valor_ori = float(valor_ori_bruto)
|
||||
except (TypeError, ValueError):
|
||||
valor_ori = 9999.0
|
||||
# Fix 4 = RTK Fixed; 5 = RTK Float. Float e degradado, nao falha.
|
||||
fix_ruim = fix not in (4, 5)
|
||||
fix_ruim_persistente = self._persistente(
|
||||
"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)
|
||||
orientacao_recebendo = (freq_ori > FREQ_MIN and not fail_ori)
|
||||
posicao_recebendo = (freq_pos > FREQ_MIN and not fail_pos)
|
||||
orientacao_valida = (orientacao_recebendo and valida_redis and status_ori == "A" and valor_ori_valido)
|
||||
correcao_expirada = (not age_valido) or age >= self.AGE_FAIL_S
|
||||
rtk_falha_persistente = self._persistente(
|
||||
"rtk_expirado",
|
||||
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 = []
|
||||
|
||||
if not conectado:
|
||||
saude = 0.0
|
||||
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:
|
||||
f_pos = min(freq_pos, FREQ_BASE)
|
||||
erro_pos = (FREQ_BASE - f_pos) / FREQ_BASE
|
||||
p_freq_pos = min(int(erro_pos * 100), 40)
|
||||
saude -= abs(p_freq_pos)
|
||||
if abs(p_freq_pos) > 0:
|
||||
motivos.append(f"Frequência de coordenadas baixa: {f_pos:.2f} Hz (penalidade {p_freq_pos}%)")
|
||||
# Ausencia persistente de sentenca e falha real de comunicacao.
|
||||
if perda_pos:
|
||||
saude = 0.0
|
||||
motivos.append(
|
||||
f"Sem coordenadas GGA recentes ha {idade_pos:.1f}s"
|
||||
)
|
||||
|
||||
f_ori = min(freq_ori, FREQ_BASE)
|
||||
erro_ori = (FREQ_BASE - f_ori) / FREQ_BASE
|
||||
p_freq_ori = min(int(erro_ori * 100), 40)
|
||||
saude -= abs(p_freq_ori)
|
||||
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
|
||||
if perda_ori:
|
||||
saude = 0.0
|
||||
motivos.append(
|
||||
f"Sem orientacao THS recente ha {idade_ori:.1f}s"
|
||||
)
|
||||
|
||||
# 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":
|
||||
motivos.append(
|
||||
"Sentença THS recebida, mas solução de heading inválida "
|
||||
f"(status V, penalidade {P_ORIENTACAO_INVALIDA}%)"
|
||||
)
|
||||
|
||||
detalhe = "status V"
|
||||
elif not valor_ori_valido:
|
||||
motivos.append(
|
||||
"Sentença THS recebida com valor de heading inválido "
|
||||
f"({valor_ori}, penalidade {P_ORIENTACAO_INVALIDA}%)"
|
||||
)
|
||||
|
||||
detalhe = f"heading invalido ({valor_ori})"
|
||||
elif not valida_ori:
|
||||
detalhe = "orientacao nao marcada como valida"
|
||||
else:
|
||||
motivos.append(
|
||||
"Sentença THS recebida, mas orientação não foi marcada "
|
||||
f"como válida (penalidade {P_ORIENTACAO_INVALIDA}%)"
|
||||
detalhe = f"status inesperado ({status_ori or 'vazio'})"
|
||||
|
||||
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
|
||||
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)
|
||||
saude = max(0.0, min(100.0, round(saude, 1)))
|
||||
|
||||
if not conectado:
|
||||
status = StatusModulo.DESCONECTADO
|
||||
|
||||
elif not posicao_recebendo or not orientacao_recebendo:
|
||||
elif perda_pos or perda_ori or saude <= 0:
|
||||
status = StatusModulo.FALHA
|
||||
|
||||
elif saude <= 0:
|
||||
status = StatusModulo.FALHA
|
||||
|
||||
elif saude < SAUDE_MIN_ALERTA:
|
||||
elif saude < self.SAUDE_MIN_ALERTA:
|
||||
status = StatusModulo.ALERTA
|
||||
|
||||
else:
|
||||
status = StatusModulo.OPERANTE
|
||||
|
||||
|
||||
saude_geral = {
|
||||
"conectado": conectado,
|
||||
"conectado": conectado,
|
||||
"status": status.value,
|
||||
"saude": saude,
|
||||
"motivos": motivos,
|
||||
"saude_idividual": []
|
||||
"saude": saude,
|
||||
"motivos": motivos,
|
||||
"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.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:
|
||||
AGE_HI = 5 # s (ruim)
|
||||
AGE_LO = 2 # s (bem estabilizado)
|
||||
HACC_OK = 40 # cm
|
||||
T_BAD = 4 # s p/ ligar NTRIP
|
||||
T_GOOD = 10 # s p/ desligar NTRIP
|
||||
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
|
||||
self._atualizar_fallback_ntrip(
|
||||
fix=fix,
|
||||
age=age,
|
||||
age_valido=age_valido,
|
||||
hacc=hacc,
|
||||
)
|
||||
|
||||
|
||||
except Exception as 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:
|
||||
if fix == 0: return 90 # inválido
|
||||
if fix == 1: return 70 # single
|
||||
if fix == 2: return 40 # DGPS
|
||||
if fix == 3: return 30 # PPS
|
||||
if fix == 4: return 0 # RTK Fixed (melhor caso)
|
||||
if fix == 5: return 5 # RTK Float (quase bom)
|
||||
if fix == 6: return 50 # dead reckoning
|
||||
if fix == 7: return 80 # manual
|
||||
if fix == 8: return 80 # simulador
|
||||
return 90
|
||||
if fix == 0:
|
||||
return 70
|
||||
if fix == 1:
|
||||
return 55
|
||||
if fix == 2:
|
||||
return 30
|
||||
if fix == 3:
|
||||
return 25
|
||||
if fix == 4:
|
||||
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:
|
||||
if h == 0: return 30
|
||||
if h < 40: return 0
|
||||
if h < 80: return 5
|
||||
if h < 120: return 10
|
||||
if h < 200: return 20
|
||||
if not math.isfinite(h) or h <= 0:
|
||||
return 15
|
||||
if h < self.HACC_WARN_CM:
|
||||
return 0
|
||||
if h < 80:
|
||||
return 5
|
||||
if h < self.HACC_BAD_CM:
|
||||
return 10
|
||||
if h < 200:
|
||||
return 20
|
||||
return 30
|
||||
|
||||
def _penalidade_age(self, age: float) -> float:
|
||||
if age < 0:
|
||||
return 35
|
||||
return round(min(35, 35 * (1 - math.exp(-age / 8))), 2)
|
||||
def _penalidade_age(self, age) -> int:
|
||||
if not math.isfinite(age) or age < 0:
|
||||
return 20
|
||||
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.
|
||||
|
||||
No contrato físico deste rover:
|
||||
- roll = inclinação frontal
|
||||
- pitch = inclinação lateral
|
||||
- roll = inclinação lateral
|
||||
- pitch = inclinação frontal
|
||||
"""
|
||||
|
||||
self.roll_attention_deg = (
|
||||
|
|
@ -777,8 +777,8 @@ class CameraIMU(ModuloDiagnosticoBase):
|
|||
if mudou:
|
||||
self.mostrar_log(
|
||||
"Limites angulares atualizados | "
|
||||
f"roll/frontal={roll_anterior:.1f}°→{self.roll_max_deg:.1f}° | "
|
||||
f"pitch/lateral={pitch_anterior:.1f}°→{self.pitch_max_deg:.1f}° | "
|
||||
f"roll/lateral={roll_anterior:.1f}°→{self.roll_max_deg:.1f}° | "
|
||||
f"pitch/frontal={pitch_anterior:.1f}°→{self.pitch_max_deg:.1f}° | "
|
||||
f"critical=({self.roll_critical_deg:.1f}°, "
|
||||
f"{self.pitch_critical_deg:.1f}°) | "
|
||||
f"emergency=({self.roll_emergency_deg:.1f}°, "
|
||||
|
|
@ -1322,13 +1322,13 @@ class CameraIMU(ModuloDiagnosticoBase):
|
|||
"emergency_stop": risco["emergency_stop"],
|
||||
|
||||
"convencao_eixos": {
|
||||
"roll": "frontal",
|
||||
"pitch": "lateral",
|
||||
"roll": "lateral",
|
||||
"pitch": "frontal",
|
||||
},
|
||||
|
||||
"limites_angulo": {
|
||||
"roll": {
|
||||
"semantica": "frontal",
|
||||
"semantica": "lateral",
|
||||
"attention_deg": round(self.roll_attention_deg, 3),
|
||||
"risk_deg": round(self.roll_risk_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),
|
||||
},
|
||||
"pitch": {
|
||||
"semantica": "lateral",
|
||||
"semantica": "frontal",
|
||||
"attention_deg": round(self.pitch_attention_deg, 3),
|
||||
"risk_deg": round(self.pitch_risk_deg, 3),
|
||||
"critical_deg": round(self.pitch_critical_deg, 3),
|
||||
|
|
|
|||
|
|
@ -29,8 +29,16 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
|
||||
SAUDE_MIN_ALERTA = 80
|
||||
|
||||
RESPOSTA_RECENTE_MS = 3000.0
|
||||
RESPOSTA_LENTA_MS = 1800.0
|
||||
# Comunicação OID: atraso passageiro não deve parar a operação. A idade da
|
||||
# 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_ALERTA_MS = 3500.0
|
||||
|
|
@ -64,6 +72,10 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
|
||||
TEMP_EMERGENCIA_CONFIRMADO_S = 1.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
|
||||
TENSAO_FALHA_PERSISTENCIA_S = 2.0
|
||||
|
|
@ -73,11 +85,21 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
# Freio/comando
|
||||
FREIO_RPM_FALHA_PERSISTENCIA_S = 1.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):
|
||||
self.t_code = T_Code.Mov
|
||||
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.freq_defaults = {
|
||||
|
|
@ -117,6 +139,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
}
|
||||
|
||||
self._estados_persistencia = {}
|
||||
self._historico_amostras = {}
|
||||
|
||||
def atualizar_saude(self):
|
||||
try:
|
||||
|
|
@ -162,7 +185,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
saude_individual.append(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))
|
||||
|
||||
if conectado:
|
||||
|
|
@ -244,7 +267,21 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
motivos = []
|
||||
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 {
|
||||
"id": endereco_str,
|
||||
"label": mod_id,
|
||||
|
|
@ -253,15 +290,35 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"motivos": ["Desconectado"],
|
||||
"condicoes_operacionais": [],
|
||||
"em_uso": comandar,
|
||||
"conectado_efetivo": False,
|
||||
"componentes": self._componentes_zero(),
|
||||
"parametros": {},
|
||||
"debug": self._snapshot_debug(modulo),
|
||||
}
|
||||
|
||||
conectividade = self._avaliar_resposta_recente(modulo, motivos, condicoes)
|
||||
comando = self._avaliar_seguimento_comando(modulo, motivos, condicoes)
|
||||
conectado_efetivo = conectado or estado_desconectado["nivel"] in (
|
||||
"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)
|
||||
freio = self._avaliar_freio(modulo, motivos, condicoes)
|
||||
freio = self._avaliar_freio(endereco_str, modulo, motivos, condicoes)
|
||||
telemetria = self._avaliar_telemetria(modulo, motivos)
|
||||
|
||||
componentes = {
|
||||
|
|
@ -303,7 +360,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
saude = int(max(0, min(100, round(saude))))
|
||||
|
||||
status = self._resolver_status_driver(
|
||||
conectado=conectado,
|
||||
conectado=conectado_efetivo,
|
||||
saude=saude,
|
||||
condicoes_operacionais=condicoes,
|
||||
)
|
||||
|
|
@ -316,6 +373,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"motivos": self._dedupe(motivos),
|
||||
"condicoes_operacionais": condicoes,
|
||||
"em_uso": comandar,
|
||||
"conectado_efetivo": conectado_efetivo,
|
||||
"componentes": componentes,
|
||||
"parametros": telemetria["parametros"],
|
||||
"detalhes_valores": valores["detalhes"],
|
||||
|
|
@ -351,10 +409,16 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
# 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)
|
||||
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)")
|
||||
condicoes.append({
|
||||
"label": "Resposta OID",
|
||||
|
|
@ -369,13 +433,30 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
})
|
||||
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:
|
||||
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}
|
||||
|
||||
def _avaliar_seguimento_comando(self, modulo, motivos, condicoes):
|
||||
def _avaliar_seguimento_comando(self, endereco, modulo, motivos, condicoes):
|
||||
comandar = self._bool(modulo.get("comandar", True))
|
||||
rpm = abs(self._float(modulo.get("rpm", 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.
|
||||
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")
|
||||
condicoes.append({
|
||||
"label": "Comando MOV",
|
||||
|
|
@ -433,6 +523,20 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
})
|
||||
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:
|
||||
# Freio é avaliado em _avaliar_freio. Aqui não cobra RPM_SP.
|
||||
return {"score": 100, "falha_seguimento": False}
|
||||
|
|
@ -573,17 +677,31 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
# Faixas absolutas. Ajustar depois com logs reais.
|
||||
if tensao > 0:
|
||||
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({
|
||||
"label": "Tensão MOV",
|
||||
"valor": tensao,
|
||||
"severidade": 92,
|
||||
"severidade": 92 if confirmada else 75,
|
||||
"descricao": f"Tensão do driver em faixa perigosa ({tensao:.1f} V)",
|
||||
"acoes": ["Verificar alimentação/bateria/conversor antes de seguir."],
|
||||
})
|
||||
score -= 70
|
||||
risco_eletrico_termico = True
|
||||
score -= 70 if confirmada else 20
|
||||
risco_eletrico_termico = risco_eletrico_termico or confirmada
|
||||
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")
|
||||
condicoes.append({
|
||||
"label": "Tensão MOV",
|
||||
|
|
@ -593,22 +711,41 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"acoes": ["Acompanhar alimentação dos drivers de movimento."],
|
||||
})
|
||||
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:
|
||||
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({
|
||||
"label": "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)",
|
||||
"acoes": [
|
||||
"Verificar roda travada, esforço mecânico ou curto.",
|
||||
"Reduzir comando imediatamente se persistir.",
|
||||
],
|
||||
})
|
||||
score -= 75
|
||||
risco_eletrico_termico = True
|
||||
score -= 75 if confirmada else 25
|
||||
risco_eletrico_termico = risco_eletrico_termico or confirmada
|
||||
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")
|
||||
condicoes.append({
|
||||
"label": "Corrente motor",
|
||||
|
|
@ -621,19 +758,38 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
],
|
||||
})
|
||||
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:
|
||||
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({
|
||||
"label": "Corrente barramento",
|
||||
"valor": corrente_barramento,
|
||||
"severidade": 90,
|
||||
"severidade": 92 if confirmada else 75,
|
||||
"descricao": f"Corrente de barramento muito alta ({corrente_barramento:.1f} A)",
|
||||
"acoes": ["Verificar alimentação e esforço dos motores."],
|
||||
})
|
||||
score -= 55
|
||||
risco_eletrico_termico = True
|
||||
score -= 55 if confirmada else 20
|
||||
risco_eletrico_termico = risco_eletrico_termico or confirmada
|
||||
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")
|
||||
condicoes.append({
|
||||
"label": "Corrente barramento",
|
||||
|
|
@ -643,6 +799,11 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"acoes": ["Acompanhar consumo e esforço do conjunto."],
|
||||
})
|
||||
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
|
||||
_penalidade_d, _motivos_d, _condicoes_d, _risco_d = (
|
||||
|
|
@ -650,6 +811,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
endereco=endereco,
|
||||
temperatura=temperatura_driver,
|
||||
regra_desc="driver",
|
||||
modulo=modulo,
|
||||
campo="temperatura_driver",
|
||||
)
|
||||
)
|
||||
motivos.extend(_motivos_d)
|
||||
|
|
@ -661,6 +824,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
endereco=endereco,
|
||||
temperatura=temperatura_motor,
|
||||
regra_desc="motor",
|
||||
modulo=modulo,
|
||||
campo="temperatura_motor",
|
||||
)
|
||||
)
|
||||
motivos.extend(_motivos_m)
|
||||
|
|
@ -710,14 +875,23 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"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))
|
||||
freado = self._bool(modulo.get("freado", False))
|
||||
freio_sp = self._bool(modulo.get("freio_sp", freado))
|
||||
comando_ligado = self._bool(modulo.get("comando_ligado", False))
|
||||
|
||||
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})")
|
||||
condicoes.append({
|
||||
"label": "Freio MOV",
|
||||
|
|
@ -735,6 +909,26 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"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:
|
||||
motivos.append("Freio ativo enquanto comando de movimento aparece ligado")
|
||||
condicoes.append({
|
||||
|
|
@ -794,6 +988,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
endereco,
|
||||
temperatura,
|
||||
regra_desc,
|
||||
modulo,
|
||||
campo,
|
||||
):
|
||||
motivos = []
|
||||
condicoes = []
|
||||
|
|
@ -808,6 +1004,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
|
||||
label = f"Temperatura do {nome_origem}"
|
||||
|
||||
amostra = self._registrar_amostra(endereco, campo, temperatura, modulo)
|
||||
|
||||
if not self._temperatura_valida(temperatura):
|
||||
motivos.append(
|
||||
f"Temperatura do {nome_origem} inválida: "
|
||||
|
|
@ -832,6 +1030,32 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
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(
|
||||
driver=endereco,
|
||||
regra=f"temperatura_emergencia_{regra_desc}",
|
||||
|
|
@ -841,6 +1065,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
tempo_intermediario_s=0.0,
|
||||
tempo_confirmacao_s=self.TEMP_EMERGENCIA_CONFIRMADO_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(
|
||||
|
|
@ -852,6 +1078,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
tempo_intermediario_s=self.TEMP_FALHA_INTERMEDIARIO_S,
|
||||
tempo_confirmacao_s=self.TEMP_FALHA_CONFIRMADO_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(
|
||||
|
|
@ -863,6 +1091,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
tempo_intermediario_s=self.TEMP_ALERTA_INTERMEDIARIO_S,
|
||||
tempo_confirmacao_s=self.TEMP_ALERTA_CONFIRMADO_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_confirmacao_s,
|
||||
tempo_recuperacao_s=0.0,
|
||||
amostra_nova=True,
|
||||
min_amostras=1,
|
||||
):
|
||||
agora = time.monotonic()
|
||||
chave = (str(driver), str(regra))
|
||||
|
|
@ -1339,6 +1571,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"ativa_desde": None,
|
||||
"normal_desde": None,
|
||||
"confirmada": False,
|
||||
"amostras_ativas": 0,
|
||||
})
|
||||
|
||||
if ativa:
|
||||
|
|
@ -1346,10 +1579,17 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
|
||||
if estado["ativa_desde"] is None:
|
||||
estado["ativa_desde"] = agora
|
||||
estado["amostras_ativas"] = 0
|
||||
|
||||
if amostra_nova:
|
||||
estado["amostras_ativas"] += 1
|
||||
|
||||
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
|
||||
|
||||
if estado["confirmada"]:
|
||||
|
|
@ -1364,9 +1604,11 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"nivel": nivel,
|
||||
"duracao_s": duracao_s,
|
||||
"confirmada": estado["confirmada"],
|
||||
"amostras": estado["amostras_ativas"],
|
||||
}
|
||||
|
||||
estado["ativa_desde"] = None
|
||||
estado["amostras_ativas"] = 0
|
||||
|
||||
if not estado["confirmada"]:
|
||||
estado["normal_desde"] = None
|
||||
|
|
@ -1375,6 +1617,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"nivel": "normal",
|
||||
"duracao_s": 0.0,
|
||||
"confirmada": False,
|
||||
"amostras": 0,
|
||||
}
|
||||
|
||||
if estado["normal_desde"] is None:
|
||||
|
|
@ -1391,6 +1634,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"nivel": "normal",
|
||||
"duracao_s": 0.0,
|
||||
"confirmada": False,
|
||||
"amostras": 0,
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -1398,6 +1642,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
"nivel": "recuperando",
|
||||
"duracao_s": normal_ha_s,
|
||||
"confirmada": True,
|
||||
"amostras": 0,
|
||||
}
|
||||
|
||||
def _estado_limite_alto(
|
||||
|
|
@ -1410,6 +1655,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
tempo_intermediario_s,
|
||||
tempo_confirmacao_s,
|
||||
tempo_recuperacao_s,
|
||||
amostra_nova=True,
|
||||
min_amostras=1,
|
||||
):
|
||||
chave = (str(driver), str(regra))
|
||||
persistencia = self._estados_persistencia.get(chave, {})
|
||||
|
|
@ -1431,6 +1678,58 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
|
|||
tempo_intermediario_s=tempo_intermediario_s,
|
||||
tempo_confirmacao_s=tempo_confirmacao_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),
|
||||
ModoOperacao.NaoDefinido,
|
||||
)
|
||||
|
||||
status_atual = cls._enum_or(
|
||||
StatusOperacao,
|
||||
operacao.get("status", StatusOperacao.NaoIniciado.value),
|
||||
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))
|
||||
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))
|
||||
calibrando = cls._bool(operacao.get("calibrando", False))
|
||||
pausa = cls._bool(operacao.get("pausa", False))
|
||||
emergencia = cls._bool(operacao.get("emergencia", 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(
|
||||
operacao.get("status_anterior_parado", StatusOperacao.Parado.value),
|
||||
StatusOperacao.Parado.value,
|
||||
)
|
||||
|
||||
updates = {
|
||||
updates_base = {
|
||||
"status_iniciado_anterior": iniciado,
|
||||
"status_atualizado_em": agora,
|
||||
}
|
||||
|
||||
if status_iniciado_anterior != iniciado:
|
||||
updates["status_iniciado_alterado_em"] = agora
|
||||
updates_base["status_iniciado_alterado_em"] = agora
|
||||
|
||||
def _commit(status, motivo, **extras):
|
||||
updates["status"] = status.value if isinstance(status, StatusOperacao) else int(status)
|
||||
updates["motivo_status_operacao"] = motivo
|
||||
novo_status = (
|
||||
status.value if isinstance(status, StatusOperacao) else int(status)
|
||||
)
|
||||
updates = dict(updates_base)
|
||||
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)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 0) Iniciando
|
||||
# ------------------------------------------------------------
|
||||
def _limpar_controle_transitorio():
|
||||
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(
|
||||
StatusOperacao.NaoIniciado,
|
||||
"Iniciando equipamento",
|
||||
StatusOperacao.Parado,
|
||||
motivo,
|
||||
status_anterior_parado=anterior,
|
||||
ultimo_motivo_parada=motivo,
|
||||
ultima_parada_em=agora,
|
||||
bloqueio_confirmado_em=agora,
|
||||
**_limpar_controle_transitorio(),
|
||||
)
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 1) Não configurada
|
||||
# ------------------------------------------------------------
|
||||
# 0) Pré-condições estruturais.
|
||||
if not equipamento_pronto:
|
||||
_commit(StatusOperacao.NaoIniciado, "Iniciando equipamento")
|
||||
return
|
||||
|
||||
if not configurado:
|
||||
_commit(
|
||||
StatusOperacao.Parametrizando,
|
||||
"Operação ainda não configurada",
|
||||
)
|
||||
_commit(StatusOperacao.Parametrizando, "Operação ainda não configurada")
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 2) Não iniciada
|
||||
# ------------------------------------------------------------
|
||||
|
||||
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:
|
||||
_commit(
|
||||
StatusOperacao.Concluido,
|
||||
"Operação concluída e não reiniciada",
|
||||
**extras,
|
||||
)
|
||||
else:
|
||||
_commit(
|
||||
StatusOperacao.Parametrizando,
|
||||
"Operação configurada, mas ainda não iniciada",
|
||||
**extras,
|
||||
)
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 3) Estados forçados
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# 1) Estados explicitamente comandados não recebem debounce.
|
||||
if finalizando:
|
||||
# Mantém compatibilidade caso não exista StatusOperacao.Finalizando.
|
||||
status_finalizando = cls._status_operacao_por_nome(
|
||||
"Finalizando",
|
||||
StatusOperacao.Parado,
|
||||
)
|
||||
|
||||
_commit(
|
||||
status_finalizando,
|
||||
"Operação em finalização",
|
||||
"Finalizando", StatusOperacao.Parado
|
||||
)
|
||||
_commit(status_finalizando, "Operação em finalização")
|
||||
return
|
||||
|
||||
if calibrando:
|
||||
_commit(
|
||||
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
|
||||
),
|
||||
)
|
||||
_commit(StatusOperacao.Calibrando, "Operação em calibração")
|
||||
return
|
||||
|
||||
if emergencia:
|
||||
_commit(
|
||||
StatusOperacao.Parado,
|
||||
"Parada de emergência solicitada pela base",
|
||||
status_anterior_parado=(
|
||||
status_atual.value
|
||||
if status_atual != StatusOperacao.Parado
|
||||
else status_anterior_parado
|
||||
),
|
||||
)
|
||||
_parar_imediatamente("Parada de emergência solicitada pela base")
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 4) Manual
|
||||
# ------------------------------------------------------------
|
||||
if pausa:
|
||||
_parar_imediatamente("Operação pausada pela base")
|
||||
return
|
||||
|
||||
# 2) Manual: mantém resposta imediata, como no comportamento original.
|
||||
if modo == ModoOperacao.Manual:
|
||||
if op_liberada:
|
||||
_commit(
|
||||
StatusOperacao.EmAndamento,
|
||||
"Operação manual liberada",
|
||||
status_anterior_parado=StatusOperacao.Parado.value,
|
||||
ja_entrou_em_andamento=True,
|
||||
**_limpar_controle_transitorio(),
|
||||
)
|
||||
else:
|
||||
_commit(
|
||||
StatusOperacao.Parado,
|
||||
cls._motivo_nao_liberado_status(operacao),
|
||||
)
|
||||
_parar_imediatamente(cls._motivo_nao_liberado_status(operacao))
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 5) Modos automáticos suportados
|
||||
# ------------------------------------------------------------
|
||||
|
||||
modos_automaticos = [
|
||||
modos_automaticos = {
|
||||
ModoOperacao.MapaGPS,
|
||||
ModoOperacao.TreinamentoIA,
|
||||
ModoOperacao.MapeamentoVisual,
|
||||
ModoOperacao.RetornoBase,
|
||||
]
|
||||
|
||||
}
|
||||
if modo not in modos_automaticos:
|
||||
_commit(
|
||||
StatusOperacao.Parado,
|
||||
f"Modo de operação sem estratégia automática: {modo.name}",
|
||||
_parar_imediatamente(
|
||||
f"Modo de operação sem estratégia automática: {modo.name}"
|
||||
)
|
||||
return
|
||||
|
||||
# Borda de início da operação.
|
||||
if iniciado and not status_iniciado_anterior:
|
||||
# 3) Inicializa uma única vez. Não depende apenas da borda de `iniciado`,
|
||||
# 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()
|
||||
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:
|
||||
motivo = cls._motivo_nao_liberado_status(operacao)
|
||||
|
||||
novo_status_anterior = status_anterior_parado
|
||||
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,
|
||||
pendente_desde = cls._float(
|
||||
operacao.get("bloqueio_pendente_desde", 0.0), 0.0
|
||||
)
|
||||
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
|
||||
|
||||
# Tempo inicial de espera.
|
||||
tempo_aguardando = cls._float(
|
||||
operacao.get("tempo_aguardando", updates.get("tempo_aguardando", 0.0)),
|
||||
0.0,
|
||||
)
|
||||
tempo_aguardar = cls._float(
|
||||
operacao.get("tempo_aguardar_inicio_operacao", 25.0),
|
||||
25.0,
|
||||
# Liberação recuperou antes da confirmação: cancela o pulso sem parar.
|
||||
bloqueio_pendente_desde = cls._float(
|
||||
operacao.get("bloqueio_pendente_desde", 0.0), 0.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:
|
||||
tempo_aguardando = agora
|
||||
updates["tempo_aguardando"] = agora
|
||||
|
||||
em_espera = (tempo_aguardando + tempo_aguardar) > agora
|
||||
|
||||
if em_espera:
|
||||
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,
|
||||
# 5) Espera inicial existe somente antes do primeiro EmAndamento.
|
||||
if not ja_entrou_em_andamento:
|
||||
tempo_aguardando = cls._float(
|
||||
operacao.get(
|
||||
"tempo_aguardando", updates_base.get("tempo_aguardando", 0.0)
|
||||
),
|
||||
0.0,
|
||||
)
|
||||
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():
|
||||
_commit(
|
||||
StatusOperacao.Concluido,
|
||||
"Trajetória concluída",
|
||||
status_anterior_parado=StatusOperacao.Parado.value,
|
||||
**_limpar_controle_transitorio(),
|
||||
)
|
||||
|
||||
if iniciado and not finalizando:
|
||||
cls._finalizar_operacao()
|
||||
|
||||
return
|
||||
|
||||
# Recuperou de Parado.
|
||||
if status_atual == StatusOperacao.Parado:
|
||||
_commit(
|
||||
StatusOperacao.Aguardando,
|
||||
"Operação liberada novamente. Reiniciando janela de segurança",
|
||||
tempo_aguardando=agora,
|
||||
status_anterior_parado=StatusOperacao.Parado.value,
|
||||
# 7) Retomada usa uma janela própria e curta; nunca reinicia a partida.
|
||||
if status_atual == StatusOperacao.Parado and ja_entrou_em_andamento:
|
||||
retomada_desde = cls._float(
|
||||
operacao.get("retomada_aguardando_desde", 0.0), 0.0
|
||||
)
|
||||
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.
|
||||
status_anterior_enum = cls._enum_or(
|
||||
StatusOperacao,
|
||||
status_anterior_parado,
|
||||
StatusOperacao.Parado,
|
||||
restante = (retomada_desde + tempo_retomada) - agora
|
||||
if restante > 0:
|
||||
_commit(
|
||||
StatusOperacao.Aguardando,
|
||||
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 status_anterior_enum in [StatusOperacao.Aguardando, StatusOperacao.EmAndamento]:
|
||||
_commit(
|
||||
status_anterior_enum,
|
||||
"Restaurando status anterior à parada",
|
||||
status_anterior_parado=StatusOperacao.Parado.value,
|
||||
if retomada_desde > 0 and ja_entrou_em_andamento:
|
||||
tempo_retomada = max(
|
||||
0.0,
|
||||
cls._float(operacao.get("tempo_aguardar_retomada", 1.0), 1.0),
|
||||
)
|
||||
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(
|
||||
StatusOperacao.EmAndamento,
|
||||
"Operação em andamento",
|
||||
motivo,
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue