ajustes em: vazao sensor de fluxo; saude ipb; definicao de controle mov; tabela ref potencia da bomba; pressao sp; saude dir; salvamento de frames visual e weed workers
This commit is contained in:
parent
66ad55aeb3
commit
bdfa707968
|
|
@ -123,7 +123,7 @@ namespace AgroBase.Models.Modules
|
|||
[JsonIgnore]
|
||||
private SensorModel SensorFluxoLinha => Sensores.FirstOrDefault(x => x.Componente == S_Code.sFLX && x.ID == "FLXLN");
|
||||
|
||||
const double FatorCalibracaoFluxo = 0.55;
|
||||
const double FatorCalibracaoFluxo = 0.85;
|
||||
|
||||
public double VazaoMaximaSensor_LMin { get; set; } = 20.0;
|
||||
|
||||
|
|
|
|||
|
|
@ -139,6 +139,21 @@ namespace AgroBase.Models.Modules
|
|||
|
||||
public Direcao UltimaDirecao { get; set; } = Direcao.Parado;
|
||||
public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue;
|
||||
|
||||
public double AnguloSPAtual
|
||||
{
|
||||
get
|
||||
{
|
||||
return !Comandar || Sentido_SP == Sentido.Parado ? 0 : Angulo_SP;
|
||||
}
|
||||
}
|
||||
public double ErroAnguloSP
|
||||
{
|
||||
get
|
||||
{
|
||||
return Math.Abs(AnguloLeitura - AnguloSPAtual);
|
||||
}
|
||||
}
|
||||
public bool RetornandoAzero
|
||||
{
|
||||
get
|
||||
|
|
@ -152,17 +167,14 @@ namespace AgroBase.Models.Modules
|
|||
{
|
||||
get
|
||||
{
|
||||
return RPM != 0;
|
||||
return Math.Abs(RPM) > 1;
|
||||
}
|
||||
}
|
||||
public bool AtingiuAnguloSP
|
||||
{
|
||||
get
|
||||
{
|
||||
double anguloSp =
|
||||
!Comandar || Sentido_SP == Sentido.Parado ? 0 :
|
||||
Angulo_SP;
|
||||
return FuncoesMatematicas.ValorEstaEntre(AnguloLeitura, anguloSp, 1);
|
||||
return FuncoesMatematicas.ValorEstaEntre(AnguloLeitura, AnguloSPAtual, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ namespace AgroBase.Models
|
|||
AtuPercentualErvasBicoOff = 2,
|
||||
AtuPercentualErvasBicoOn = 1,
|
||||
AtuPercentualInicioPulverizacao = 85,
|
||||
AtuPressaoLinha = 22,
|
||||
AtuPressaoLinha = 18,
|
||||
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
|
||||
AtuModoControle = ModoControleBomba.PID
|
||||
};
|
||||
|
|
@ -629,7 +629,7 @@ namespace AgroBase.Models
|
|||
AtuPercentualErvasBicoOff = 2,
|
||||
AtuPercentualErvasBicoOn = 1,
|
||||
AtuPercentualInicioPulverizacao = 85,
|
||||
AtuPressaoLinha = 22,
|
||||
AtuPressaoLinha = 18,
|
||||
AtuAgitadorModo = ModoAgitadorCalda.Continuo,
|
||||
AtuModoControle = ModoControleBomba.PID,
|
||||
};
|
||||
|
|
@ -768,8 +768,8 @@ namespace AgroBase.Models
|
|||
AtuDuracaoAtuacao = 200,
|
||||
AtuPercentualErvasBicoOff = 2,
|
||||
AtuPercentualErvasBicoOn = 1,
|
||||
AtuPercentualInicioPulverizacao = 70,
|
||||
AtuPressaoLinha = 22,
|
||||
AtuPercentualInicioPulverizacao = 85,
|
||||
AtuPressaoLinha = 18,
|
||||
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
|
||||
AtuModoControle = ModoControleBomba.PID,
|
||||
};
|
||||
|
|
@ -908,7 +908,7 @@ namespace AgroBase.Models
|
|||
AtuPercentualErvasBicoOff = 2,
|
||||
AtuPercentualErvasBicoOn = 1,
|
||||
AtuPercentualInicioPulverizacao = 85,
|
||||
AtuPressaoLinha = 22,
|
||||
AtuPressaoLinha = 18,
|
||||
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
|
||||
AtuModoControle = ModoControleBomba.PID,
|
||||
};
|
||||
|
|
@ -1007,7 +1007,7 @@ namespace AgroBase.Models
|
|||
AtuPercentualErvasBicoOff = 2,
|
||||
AtuPercentualErvasBicoOn = 1,
|
||||
AtuPercentualInicioPulverizacao = 85,
|
||||
AtuPressaoLinha = 22,
|
||||
AtuPressaoLinha = 18,
|
||||
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
|
||||
AtuModoControle = ModoControleBomba.PID,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -95,12 +95,12 @@ namespace AgroBase.Services
|
|||
*/
|
||||
{ MksFuncCode.PulsosRecebidos, TimeSpan.FromMilliseconds(70) },
|
||||
{ MksFuncCode.IO, TimeSpan.FromMilliseconds(700) },
|
||||
{ MksFuncCode.RPM, TimeSpan.FromMilliseconds(500) },
|
||||
|
||||
/*
|
||||
* Diagnóstico opcional.
|
||||
* Não entram em OrdemPolling por padrão.
|
||||
*/
|
||||
{ MksFuncCode.RPM, TimeSpan.FromMilliseconds(500) },
|
||||
{ MksFuncCode.ErroAcumulado, TimeSpan.FromSeconds(1) },
|
||||
{ MksFuncCode.EncoderAcumulado, TimeSpan.FromSeconds(1) },
|
||||
{ MksFuncCode.EncoderCarregado, TimeSpan.FromSeconds(2) },
|
||||
|
|
@ -965,6 +965,15 @@ namespace AgroBase.Services
|
|||
return (ushort)((data[startIndex] << 8) | data[startIndex + 1]);
|
||||
}
|
||||
|
||||
private static short DecodeInt16BE(byte[] data, int startIndex)
|
||||
{
|
||||
if (data == null || data.Length < startIndex + 2)
|
||||
return 0;
|
||||
|
||||
ushort raw = (ushort)((data[startIndex] << 8) | data[startIndex + 1]);
|
||||
return unchecked((short)raw);
|
||||
}
|
||||
|
||||
private static int DecodeInt32BE(byte[] data, int startIndex)
|
||||
{
|
||||
if (data == null || data.Length < startIndex + 4)
|
||||
|
|
@ -1213,7 +1222,9 @@ namespace AgroBase.Services
|
|||
if (data == null || data.Length < 3)
|
||||
return;
|
||||
|
||||
item.RPM.valor = DecodeUInt16BE(data, 1);
|
||||
short rpm = DecodeInt16BE(data, 1);
|
||||
|
||||
item.RPM.valor = rpm;
|
||||
}
|
||||
|
||||
private void ProcessarPulsosRecebidos(MKS057DModel item, byte[] data)
|
||||
|
|
|
|||
|
|
@ -308,6 +308,9 @@ namespace AgroBase.Services.Operadores
|
|||
{
|
||||
percent_vel_max = pControle.MovVelocidadeSErvasPercent,
|
||||
percent_vel_min = pControle.MovVelocidadeCErvasPercent,
|
||||
rampa_aceleracao_pct_s = 12.0,
|
||||
rampa_desaceleracao_pct_s = 18.0,
|
||||
rampa_desaceleracao_seguranca_pct_s = 35.0,
|
||||
}
|
||||
),
|
||||
("Dir", new
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class ModuloDirecional(ModuloDiagnosticoBase):
|
|||
self.t_code = T_Code.Dir
|
||||
self.nome = "Direcional"
|
||||
self.timeout = 3
|
||||
self._estado_driver = {}
|
||||
|
||||
def atualizar_saude(self):
|
||||
try:
|
||||
|
|
@ -179,8 +180,8 @@ class ModuloDirecional(ModuloDiagnosticoBase):
|
|||
|
||||
leitura = self._avaliar_resposta_recente(modulo, motivos, condicoes)
|
||||
feedback = self._avaliar_feedback(modulo, motivos, condicoes, referenciando)
|
||||
comando = self._avaliar_seguimento_comando(modulo, motivos, condicoes)
|
||||
coerencia = self._avaliar_coerencia_operacional(modulo, motivos, condicoes)
|
||||
comando = self._avaliar_seguimento_comando(endereco_str, modulo, motivos, condicoes)
|
||||
coerencia = self._avaliar_coerencia_operacional(endereco_str, modulo, motivos, condicoes)
|
||||
telemetria = self._avaliar_telemetria(modulo, motivos, referenciando)
|
||||
|
||||
componentes = {
|
||||
|
|
@ -336,62 +337,116 @@ class ModuloDirecional(ModuloDiagnosticoBase):
|
|||
"angulo_sp": angulo_sp,
|
||||
}
|
||||
|
||||
def _avaliar_seguimento_comando(self, modulo, motivos, condicoes):
|
||||
def _avaliar_seguimento_comando(self, endereco_str, modulo, motivos, condicoes):
|
||||
st = self._estado(endereco_str)
|
||||
|
||||
comandar = self._bool(modulo.get("comandar", True))
|
||||
atingiu = self._bool(modulo.get("atingiu_angulo_sp", False))
|
||||
retornando_a_zero = self._bool(modulo.get("retornando_a_zero", False))
|
||||
em_movimento = self._bool(modulo.get("em_movimento", False))
|
||||
|
||||
erro_angulo = self._float(
|
||||
modulo.get(
|
||||
"erro_angulo",
|
||||
abs(
|
||||
self._float(modulo.get("angulo", 0.0), 0.0) -
|
||||
self._float(modulo.get("angulo_sp", 0.0), 0.0)
|
||||
)
|
||||
),
|
||||
0.0,
|
||||
)
|
||||
angulo = self._float(modulo.get("angulo", 0.0), 0.0)
|
||||
angulo_sp = self._float(modulo.get("angulo_sp", 0.0), 0.0)
|
||||
erro_angulo = abs(angulo_sp - angulo)
|
||||
|
||||
ultimo_comando_ms = self._float(modulo.get("ultimo_comando_ms", 999999), 999999)
|
||||
ultima_direcao = self._safe_int(
|
||||
modulo.get("ultima_direcao", self._direcao_parada_value()),
|
||||
self._direcao_parada_value(),
|
||||
ultimo_comando_ms = self._float(
|
||||
modulo.get("ultimo_comando_ms", 999999),
|
||||
999999
|
||||
)
|
||||
|
||||
if not comandar:
|
||||
# Driver conectado, mas não participa do comando atual.
|
||||
return {
|
||||
"score": 100,
|
||||
"falha_seguimento": False,
|
||||
}
|
||||
|
||||
if atingiu:
|
||||
if atingiu or erro_angulo <= self.TOLERANCIA_ANGULO_PADRAO_GRAUS:
|
||||
st["ultimo_erro"] = erro_angulo
|
||||
st["ultimo_sp"] = angulo_sp
|
||||
st["ultimo_comando_ms"] = ultimo_comando_ms
|
||||
|
||||
return {
|
||||
"score": 100,
|
||||
"falha_seguimento": False,
|
||||
}
|
||||
|
||||
if ultimo_comando_ms < self.COMANDO_GRACE_MS:
|
||||
sp_anterior = st.get("ultimo_sp")
|
||||
erro_anterior = st.get("ultimo_erro")
|
||||
comando_ms_anterior = st.get("ultimo_comando_ms")
|
||||
|
||||
sp_mudou = (
|
||||
sp_anterior is None or
|
||||
abs(angulo_sp - self._float(sp_anterior, angulo_sp)) > 0.5
|
||||
)
|
||||
|
||||
comando_reiniciou = (
|
||||
comando_ms_anterior is None or
|
||||
ultimo_comando_ms < self._float(comando_ms_anterior, ultimo_comando_ms)
|
||||
)
|
||||
|
||||
erro_diminuindo = (
|
||||
erro_anterior is not None and
|
||||
erro_angulo < self._float(erro_anterior, erro_angulo) - 0.3
|
||||
)
|
||||
|
||||
# Estima janela maior quando o salto angular é grande.
|
||||
delta_sp = abs(angulo_sp - self._float(sp_anterior, angulo_sp)) if sp_anterior is not None else erro_angulo
|
||||
|
||||
grace_ms = max(self.COMANDO_GRACE_MS, min(2500.0, 500.0 + delta_sp * 35.0))
|
||||
alerta_ms = max(self.COMANDO_ALERTA_MS, min(7000.0, 1200.0 + delta_sp * 80.0))
|
||||
falha_ms = max(self.COMANDO_FALHA_MS, min(12000.0, 2500.0 + delta_sp * 140.0))
|
||||
|
||||
st["ultimo_erro"] = erro_angulo
|
||||
st["ultimo_sp"] = angulo_sp
|
||||
st["ultimo_comando_ms"] = ultimo_comando_ms
|
||||
|
||||
# Transição esperada.
|
||||
if sp_mudou or comando_reiniciou or ultimo_comando_ms < grace_ms:
|
||||
return {
|
||||
"score": 96,
|
||||
"falha_seguimento": False,
|
||||
}
|
||||
|
||||
if retornando_a_zero and erro_angulo <= self.ERRO_ALERTA_GRAUS:
|
||||
motivos.append(
|
||||
f"Retornando a zero com erro residual ({erro_angulo:.2f}°)"
|
||||
)
|
||||
# Ainda longe, mas mexendo ou melhorando.
|
||||
if ultimo_comando_ms < alerta_ms and (em_movimento or erro_diminuindo):
|
||||
return {
|
||||
"score": 88,
|
||||
"score": 92,
|
||||
"falha_seguimento": False,
|
||||
}
|
||||
|
||||
# Aqui começa a diferença entre telemetria lenta e comando que não pegou.
|
||||
if ultimo_comando_ms >= self.COMANDO_FALHA_MS and erro_angulo >= self.ERRO_FALHA_GRAUS:
|
||||
# Alerta real: demorou e ainda está longe, mas não é falha dura.
|
||||
if ultimo_comando_ms >= alerta_ms and erro_angulo >= self.ERRO_ALERTA_GRAUS:
|
||||
motivos.append(
|
||||
f"Ângulo ainda distante do SP "
|
||||
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
|
||||
)
|
||||
|
||||
condicoes.append({
|
||||
"valor": erro_angulo,
|
||||
"severidade": min(89, max(45, erro_angulo * 5)),
|
||||
"descricao": (
|
||||
"Ângulo direcional distante do SP após janela dinâmica "
|
||||
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
|
||||
),
|
||||
})
|
||||
|
||||
return {
|
||||
"score": 72,
|
||||
"falha_seguimento": False,
|
||||
}
|
||||
|
||||
# Falha real: comando antigo, erro alto e sem sinal de convergência.
|
||||
if (
|
||||
ultimo_comando_ms >= falha_ms and
|
||||
erro_angulo >= self.ERRO_FALHA_GRAUS and
|
||||
not em_movimento and
|
||||
not erro_diminuindo
|
||||
):
|
||||
motivos.append(
|
||||
f"Comando direcional não convergiu "
|
||||
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
|
||||
)
|
||||
|
||||
condicoes.append({
|
||||
"valor": erro_angulo,
|
||||
"severidade": 92,
|
||||
|
|
@ -400,55 +455,44 @@ class ModuloDirecional(ModuloDiagnosticoBase):
|
|||
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
|
||||
),
|
||||
})
|
||||
|
||||
return {
|
||||
"score": 20,
|
||||
"falha_seguimento": True,
|
||||
}
|
||||
|
||||
if ultimo_comando_ms >= self.COMANDO_ALERTA_MS and erro_angulo >= self.ERRO_ALERTA_GRAUS:
|
||||
if retornando_a_zero and erro_angulo <= self.ERRO_ALERTA_GRAUS:
|
||||
motivos.append(
|
||||
f"Ângulo ainda distante do SP "
|
||||
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
|
||||
f"Retornando a zero com erro residual ({erro_angulo:.2f}°)"
|
||||
)
|
||||
condicoes.append({
|
||||
"valor": erro_angulo,
|
||||
"severidade": min(89, max(40, erro_angulo * 5)),
|
||||
"descricao": (
|
||||
"Ângulo direcional distante do SP, mas ainda dentro de janela de alerta "
|
||||
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
|
||||
),
|
||||
})
|
||||
return {
|
||||
"score": 70,
|
||||
"falha_seguimento": False,
|
||||
}
|
||||
|
||||
if erro_angulo > self.TOLERANCIA_ANGULO_PADRAO_GRAUS:
|
||||
motivos.append(
|
||||
f"Ângulo fora da tolerância momentaneamente (erro={erro_angulo:.2f}°)"
|
||||
)
|
||||
return {
|
||||
"score": 90,
|
||||
"score": 88,
|
||||
"falha_seguimento": False,
|
||||
}
|
||||
|
||||
return {
|
||||
"score": 100,
|
||||
"score": 90,
|
||||
"falha_seguimento": False,
|
||||
}
|
||||
|
||||
def _avaliar_coerencia_operacional(self, modulo, motivos, condicoes):
|
||||
def _avaliar_coerencia_operacional(self, endereco_str, modulo, motivos, condicoes):
|
||||
st = self._estado(endereco_str)
|
||||
|
||||
referenciado = self._bool(modulo.get("referenciado", False))
|
||||
referenciando = self._bool(modulo.get("referenciando", False))
|
||||
atingiu = self._bool(modulo.get("atingiu_angulo_sp", False))
|
||||
em_movimento = self._bool(modulo.get("em_movimento", False))
|
||||
|
||||
angulo = self._float(modulo.get("angulo", 0.0), 0.0)
|
||||
angulo_sp = self._float(modulo.get("angulo_sp", 0.0), 0.0)
|
||||
erro_angulo = abs(angulo_sp - angulo)
|
||||
|
||||
sentido = self._safe_int(
|
||||
modulo.get("sentido", self._sentido_parado_value()),
|
||||
self._sentido_parado_value(),
|
||||
)
|
||||
sentido_real = self._safe_int(
|
||||
modulo.get("sentido_real", self._sentido_parado_value()),
|
||||
self._sentido_parado_value(),
|
||||
)
|
||||
|
||||
sentido_sp = self._safe_int(
|
||||
modulo.get("sentido_sp", self._sentido_parado_value()),
|
||||
self._sentido_parado_value(),
|
||||
|
|
@ -466,24 +510,46 @@ class ModuloDirecional(ModuloDiagnosticoBase):
|
|||
})
|
||||
score = min(score, 75)
|
||||
|
||||
# Sentido real vem do sensor do rasgo.
|
||||
# Como esse sensor pode ter borda/transição, não vamos bloquear operação por isso sozinho.
|
||||
if sentido != self._sentido_parado_value() and sentido != sentido_real:
|
||||
motivos.append(
|
||||
f"Sentido real incoerente "
|
||||
f"(real={self._nome_sentido(sentido_real)}, esperado={self._nome_sentido(sentido)})"
|
||||
)
|
||||
condicoes.append({
|
||||
"valor": sentido_real,
|
||||
"severidade": 70,
|
||||
"descricao": (
|
||||
"Sentido real difere do sentido calculado "
|
||||
f"(real={self._nome_sentido(sentido_real)}, "
|
||||
f"esperado={self._nome_sentido(sentido)}, "
|
||||
f"SP={self._nome_sentido(sentido_sp)})"
|
||||
),
|
||||
})
|
||||
score = min(score, 70)
|
||||
# Durante movimento/transição, não julga sentido real.
|
||||
if em_movimento or not atingiu or erro_angulo > 3.0:
|
||||
st["incoerencia_sentido_count"] = 0
|
||||
return {
|
||||
"score": score,
|
||||
"falha_coerencia": False,
|
||||
}
|
||||
|
||||
# Perto de zero, o sentido/lado não é confiável.
|
||||
if abs(angulo_sp) < 2.0:
|
||||
st["incoerencia_sentido_count"] = 0
|
||||
return {
|
||||
"score": score,
|
||||
"falha_coerencia": False,
|
||||
}
|
||||
|
||||
# Só compara quando está parado/estável no SP.
|
||||
if sentido_sp != self._sentido_parado_value() and sentido_real != sentido_sp:
|
||||
st["incoerencia_sentido_count"] = st.get("incoerencia_sentido_count", 0) + 1
|
||||
|
||||
# Histerese: precisa repetir.
|
||||
if st["incoerencia_sentido_count"] >= 3:
|
||||
motivos.append(
|
||||
f"Sensor de lado/sentido incoerente em posição estável "
|
||||
f"(real={self._nome_sentido(sentido_real)}, esperado={self._nome_sentido(sentido_sp)})"
|
||||
)
|
||||
|
||||
condicoes.append({
|
||||
"valor": sentido_real,
|
||||
"severidade": 65,
|
||||
"descricao": (
|
||||
"Sensor de lado/sentido difere do esperado após estabilização "
|
||||
f"(real={self._nome_sentido(sentido_real)}, "
|
||||
f"esperado={self._nome_sentido(sentido_sp)})"
|
||||
),
|
||||
})
|
||||
|
||||
score = min(score, 75)
|
||||
else:
|
||||
st["incoerencia_sentido_count"] = 0
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
|
|
@ -812,3 +878,14 @@ class ModuloDirecional(ModuloDiagnosticoBase):
|
|||
return Sentido(valor).name
|
||||
except Exception:
|
||||
return str(valor)
|
||||
|
||||
def _estado(self, endereco_str):
|
||||
if endereco_str not in self._estado_driver:
|
||||
self._estado_driver[endereco_str] = {
|
||||
"ultimo_erro": None,
|
||||
"ultimo_sp": None,
|
||||
"ultimo_comando_ms": None,
|
||||
"incoerencia_sentido_count": 0,
|
||||
}
|
||||
|
||||
return self._estado_driver[endereco_str]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
nunca pela frequência do loop do Health Worker.
|
||||
4. Falha crítica exige persistência ou confirmação por mais de uma evidência.
|
||||
5. Banda estimada é indício de pressão, não prova isolada de falha.
|
||||
6. O payload antigo é preservado na medida do possível para não quebrar
|
||||
6. Jitter isolado gera alerta e redução operacional, mas não bloqueio duro.
|
||||
7. O payload antigo é preservado na medida do possível para não quebrar
|
||||
consumidores existentes.
|
||||
"""
|
||||
|
||||
|
|
@ -1123,30 +1124,66 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
math.isfinite(hb_age) and hb_age >= self._heartbeat_critical_s
|
||||
)
|
||||
|
||||
# Perda percentual só vira evidência após uma janela mínima e pelo
|
||||
# menos duas falhas reais. Isso impede que 1 falha isolada durante o
|
||||
# aquecimento apareça como 16,7% e dispare degradação.
|
||||
# Perda percentual só vira evidência após janela mínima e pelo menos
|
||||
# duas falhas reais. Uma falha isolada não degrada o enlace.
|
||||
loss_degraded = (
|
||||
metrics["probe_sample_count"] >= 12
|
||||
and metrics["failures_s"] >= 2
|
||||
and loss_s >= 10.0
|
||||
)
|
||||
|
||||
qos_degraded = probe_ready and (
|
||||
loss_degraded
|
||||
or (rtt_s >= 180.0 and jitter_s >= 50.0)
|
||||
or jitter_s >= 100.0
|
||||
or self._consecutive_ping_failures >= 2
|
||||
# Jitter sozinho vira aviso operacional, não prova de perda de enlace.
|
||||
# No teste de 03/07, havia jitter alto, mas perda 0%, RTT baixo,
|
||||
# heartbeat ok e ping recente. Esse cenário deve reduzir tráfego,
|
||||
# não travar avanço.
|
||||
jitter_warning = bool(
|
||||
probe_ready
|
||||
and jitter_s >= 80.0
|
||||
)
|
||||
|
||||
corroborated_service_delay = heartbeat_delayed and (
|
||||
loss_s >= 5.0
|
||||
or rtt_s >= 150.0
|
||||
or jitter_s >= 60.0
|
||||
or self._consecutive_ping_failures >= 2
|
||||
jitter_isolated = bool(
|
||||
jitter_warning
|
||||
and loss_s <= 2.0
|
||||
and rtt_s <= 120.0
|
||||
and heartbeat_snapshot["heartbeat_ok"]
|
||||
and evidence["strong_positive"]
|
||||
and self._consecutive_ping_failures == 0
|
||||
)
|
||||
|
||||
bandwidth_corroborated = (
|
||||
jitter_degraded = bool(
|
||||
probe_ready
|
||||
and jitter_s >= 100.0
|
||||
and not jitter_isolated
|
||||
and (
|
||||
rtt_s >= 120.0
|
||||
or loss_s >= 5.0
|
||||
or heartbeat_delayed
|
||||
or self._consecutive_ping_failures >= 1
|
||||
or not evidence["strong_positive"]
|
||||
)
|
||||
)
|
||||
|
||||
qos_degraded = bool(
|
||||
probe_ready
|
||||
and (
|
||||
loss_degraded
|
||||
or (rtt_s >= 180.0 and jitter_s >= 50.0)
|
||||
or jitter_degraded
|
||||
or self._consecutive_ping_failures >= 2
|
||||
)
|
||||
)
|
||||
|
||||
corroborated_service_delay = bool(
|
||||
heartbeat_delayed
|
||||
and (
|
||||
loss_s >= 5.0
|
||||
or rtt_s >= 150.0
|
||||
or jitter_s >= 60.0
|
||||
or self._consecutive_ping_failures >= 2
|
||||
)
|
||||
)
|
||||
|
||||
bandwidth_corroborated = bool(
|
||||
metrics["bw_util_s"] >= 85.0
|
||||
and (
|
||||
loss_s >= 5.0
|
||||
|
|
@ -1184,7 +1221,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
)
|
||||
)
|
||||
|
||||
# Confirmação de queda total: nunca depende apenas de MQTT.
|
||||
# Queda total nunca depende apenas de MQTT.
|
||||
down_candidate = bool(
|
||||
not evidence["route_ok"]
|
||||
or (
|
||||
|
|
@ -1194,6 +1231,21 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
)
|
||||
)
|
||||
|
||||
# Sinal de degradação que realmente merece restringir avanço.
|
||||
# DEGRADED por perda, RTT, heartbeat ou ping falhando é diferente
|
||||
# de jitter isolado com controle remoto comprovadamente vivo.
|
||||
degraded_restrictive_signal = bool(
|
||||
degraded_now
|
||||
and (
|
||||
loss_s >= 5.0
|
||||
or rtt_s >= 150.0
|
||||
or heartbeat_delayed
|
||||
or self._consecutive_ping_failures >= 1
|
||||
or not evidence["strong_positive"]
|
||||
or not heartbeat_snapshot["heartbeat_ok"]
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"degraded_now": degraded_now,
|
||||
"critical_now": critical_now,
|
||||
|
|
@ -1202,9 +1254,12 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
"heartbeat_very_delayed": heartbeat_very_delayed,
|
||||
"bandwidth_corroborated": bandwidth_corroborated,
|
||||
"qos_degraded": qos_degraded,
|
||||
"jitter_warning": jitter_warning,
|
||||
"jitter_isolated": jitter_isolated,
|
||||
"jitter_degraded": jitter_degraded,
|
||||
"degraded_restrictive_signal": degraded_restrictive_signal,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _elapsed_since(now_mono: float, started_mono: float) -> float:
|
||||
return max(0.0, now_mono - started_mono) if started_mono > 0 else 0.0
|
||||
|
||||
|
|
@ -1300,7 +1355,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
else:
|
||||
bw_effective_score = max(85.0, metrics["bw_score_raw"])
|
||||
|
||||
# Reachability: usa várias evidências. MQTT sozinho não decide nada.
|
||||
# Reachability usa várias evidências. MQTT sozinho não decide nada.
|
||||
if evidence["strong_positive"]:
|
||||
reachability_score = 100.0
|
||||
elif evidence["positive_count"] >= 2:
|
||||
|
|
@ -1308,7 +1363,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
elif evidence["any_positive"]:
|
||||
reachability_score = 80.0
|
||||
elif not metrics["probe_ready"] and evidence["route_ok"]:
|
||||
reachability_score = 75.0 # aquecimento
|
||||
reachability_score = 75.0
|
||||
elif evidence["route_ok"]:
|
||||
reachability_score = 30.0
|
||||
else:
|
||||
|
|
@ -1323,8 +1378,6 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
+ 0.03 * bw_effective_score
|
||||
)
|
||||
|
||||
# Serviço MQTT/heartbeat pode reduzir um pouco a saúde observada, mas
|
||||
# jamais transformar sozinho um enlace comprovadamente ativo em falha.
|
||||
service_penalty = 0.0
|
||||
if not heartbeat_snapshot["mqtt_connected"]:
|
||||
service_penalty += 4.0
|
||||
|
|
@ -1336,7 +1389,13 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
if self.link_state == self.LINK_CRITICAL:
|
||||
health = min(health, 45.0)
|
||||
elif self.link_state == self.LINK_DEGRADED:
|
||||
health = min(health, 75.0)
|
||||
# Se o degradado é restritivo, mantém cap forte. Se é apenas um
|
||||
# degradado brando com controle vivo, cap menos agressivo para
|
||||
# não virar bloqueio indireto por score.
|
||||
if instant.get("degraded_restrictive_signal", True):
|
||||
health = min(health, 75.0)
|
||||
else:
|
||||
health = min(health, 82.0)
|
||||
|
||||
return {
|
||||
"health": round(health, 1),
|
||||
|
|
@ -1463,6 +1522,76 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
self._max_distancia_desde_degradado = 0.0
|
||||
self._max_distancia_ok = max(self._max_distancia_ok, distancia_base)
|
||||
|
||||
|
||||
def _decisao_operacional_rede(
|
||||
self,
|
||||
conectado: bool,
|
||||
metrics,
|
||||
evidence,
|
||||
heartbeat_snapshot,
|
||||
instant,
|
||||
down_confirmed: bool,
|
||||
):
|
||||
rtt_s = metrics["avg_rtt_s"] or 0.0
|
||||
loss_s = metrics["loss_s"]
|
||||
|
||||
controle_remoto_estavel = bool(
|
||||
conectado
|
||||
and evidence["route_ok"]
|
||||
and evidence["strong_positive"]
|
||||
and heartbeat_snapshot["heartbeat_ok"]
|
||||
and loss_s <= 2.0
|
||||
and rtt_s <= 120.0
|
||||
and self._consecutive_ping_failures == 0
|
||||
)
|
||||
|
||||
link_critico = bool(
|
||||
not conectado
|
||||
or down_confirmed
|
||||
or self.link_state == self.LINK_CRITICAL
|
||||
or instant["critical_now"]
|
||||
or instant["down_candidate"]
|
||||
)
|
||||
|
||||
degradado_restritivo = bool(
|
||||
conectado
|
||||
and self.link_state == self.LINK_DEGRADED
|
||||
and instant.get("degraded_restrictive_signal", False)
|
||||
and not controle_remoto_estavel
|
||||
)
|
||||
|
||||
pode_avancar_com_restricao = bool(
|
||||
conectado
|
||||
and self.link_state == self.LINK_DEGRADED
|
||||
and not link_critico
|
||||
and not degradado_restritivo
|
||||
and controle_remoto_estavel
|
||||
)
|
||||
|
||||
deve_reduzir_trafego = bool(
|
||||
instant.get("jitter_warning", False)
|
||||
or instant["degraded_now"]
|
||||
or instant["bandwidth_corroborated"]
|
||||
or self.link_state != self.LINK_OK
|
||||
)
|
||||
|
||||
deve_limitar_velocidade = bool(
|
||||
deve_reduzir_trafego
|
||||
and not link_critico
|
||||
)
|
||||
|
||||
return {
|
||||
"controle_remoto_estavel": controle_remoto_estavel,
|
||||
"link_critico": link_critico,
|
||||
"degradado_restritivo": degradado_restritivo,
|
||||
"pode_avancar": bool(conectado and not link_critico and not degradado_restritivo),
|
||||
"pode_avancar_com_restricao": pode_avancar_com_restricao,
|
||||
"deve_conter_avanco": bool(conectado and self.link_state == self.LINK_DEGRADED),
|
||||
"deve_limitar_velocidade": deve_limitar_velocidade,
|
||||
"deve_reduzir_trafego": deve_reduzir_trafego,
|
||||
"deve_parar_por_rede": link_critico,
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# PAYLOAD / APRESENTAÇÃO
|
||||
# ======================================================================
|
||||
|
|
@ -1518,6 +1647,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
down_confirmed,
|
||||
regra_distancia_ativa,
|
||||
distancia_base,
|
||||
decisao_operacional,
|
||||
):
|
||||
motivos = []
|
||||
condicoes = []
|
||||
|
|
@ -1636,6 +1766,31 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
condicoes.append(condition)
|
||||
jitter_cond.append(condition)
|
||||
|
||||
if instant.get("jitter_warning", False) and not instant.get("jitter_degraded", False):
|
||||
text = (
|
||||
f"Jitter elevado isolado: fast {metrics['jitter_f']:.1f} ms; "
|
||||
f"slow {metrics['jitter_s']:.1f} ms. Enlace ainda possui "
|
||||
"ping/heartbeat ativos, sem perda relevante."
|
||||
)
|
||||
motivos.append(text)
|
||||
jitter_motivos.append(text)
|
||||
condition = {
|
||||
"label": "Jitter elevado isolado",
|
||||
"valor": round(metrics["jitter_f"], 2),
|
||||
"severidade": 55,
|
||||
"descricao": (
|
||||
"Jitter alto sem perda, sem RTT alto e com evidência forte "
|
||||
"de alcance. Deve reduzir tráfego e velocidade, não parar."
|
||||
),
|
||||
"acoes": [
|
||||
"Reduzir bitrate/FPS dos streams",
|
||||
"Evitar salvamento pesado de imagens",
|
||||
"Manter controle, RTCM e heartbeat priorizados",
|
||||
],
|
||||
}
|
||||
condicoes.append(condition)
|
||||
jitter_cond.append(condition)
|
||||
|
||||
self._append_individual(
|
||||
individuais,
|
||||
"ip_jitter",
|
||||
|
|
@ -1789,6 +1944,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
"acoes": [
|
||||
"Reduzir tráfego não crítico",
|
||||
"Priorizar controle, RTCM e heartbeat",
|
||||
"Permitir avanço restrito se controle remoto estiver estável",
|
||||
"Permitir retorno ou manutenção de posição",
|
||||
],
|
||||
}
|
||||
|
|
@ -1830,9 +1986,14 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
}
|
||||
)
|
||||
|
||||
dist_violando_cerca = (
|
||||
distancia_restritiva_ativa = bool(
|
||||
regra_distancia_ativa
|
||||
and self.link_state == self.LINK_DEGRADED
|
||||
and decisao_operacional.get("degradado_restritivo", False)
|
||||
)
|
||||
|
||||
dist_violando_cerca = (
|
||||
distancia_restritiva_ativa
|
||||
and self._distancia_inicio_degradado is not None
|
||||
and distancia_base is not None
|
||||
and distancia_base > self._distancia_inicio_degradado + 5.0
|
||||
|
|
@ -1929,6 +2090,15 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
else:
|
||||
status = StatusModulo.OPERANTE
|
||||
|
||||
decisao_operacional = self._decisao_operacional_rede(
|
||||
conectado,
|
||||
metrics,
|
||||
evidence,
|
||||
heartbeat_snapshot,
|
||||
instant,
|
||||
down_confirmed,
|
||||
)
|
||||
|
||||
motivos, condicoes, saude_individual, risk_score = self._build_messages(
|
||||
metrics,
|
||||
heartbeat_snapshot,
|
||||
|
|
@ -1938,6 +2108,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
down_confirmed,
|
||||
regra_distancia_ativa,
|
||||
distancia_base,
|
||||
decisao_operacional,
|
||||
)
|
||||
|
||||
degraded_for_s = self._elapsed_since(now_mono, self._degraded_since_mono)
|
||||
|
|
@ -1985,11 +2156,17 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
"cycles_degraded": self._cycles_degraded,
|
||||
"cycles_critical": self._cycles_critical,
|
||||
"cycles_recovered": self._cycles_recovered,
|
||||
"pode_avancar": conectado and self.link_state == self.LINK_OK,
|
||||
"deve_conter_avanco": conectado and self.link_state == self.LINK_DEGRADED,
|
||||
"deve_parar_por_rede": (
|
||||
not conectado or self.link_state == self.LINK_CRITICAL
|
||||
),
|
||||
# Chaves antigas preservadas, mas com semântica menos ansiosa.
|
||||
# DEGRADED não trava avanço por si só. Só trava se for
|
||||
# degradado restritivo ou crítico confirmado.
|
||||
"pode_avancar": decisao_operacional["pode_avancar"],
|
||||
"pode_avancar_com_restricao": decisao_operacional["pode_avancar_com_restricao"],
|
||||
"deve_conter_avanco": decisao_operacional["deve_conter_avanco"],
|
||||
"deve_limitar_velocidade": decisao_operacional["deve_limitar_velocidade"],
|
||||
"deve_reduzir_trafego": decisao_operacional["deve_reduzir_trafego"],
|
||||
"degradado_restritivo": decisao_operacional["degradado_restritivo"],
|
||||
"controle_remoto_estavel": decisao_operacional["controle_remoto_estavel"],
|
||||
"deve_parar_por_rede": decisao_operacional["deve_parar_por_rede"],
|
||||
"distancia_inicio_degradado": self._distancia_inicio_degradado,
|
||||
"distancia_inicio_critico": self._distancia_inicio_critico,
|
||||
"max_distancia_ok": self._max_distancia_ok,
|
||||
|
|
@ -2052,6 +2229,13 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
|
|||
"bandwidth_corroborated": instant[
|
||||
"bandwidth_corroborated"
|
||||
],
|
||||
"jitter_warning": instant.get("jitter_warning", False),
|
||||
"jitter_isolated": instant.get("jitter_isolated", False),
|
||||
"jitter_degraded": instant.get("jitter_degraded", False),
|
||||
"degraded_restrictive_signal": instant.get(
|
||||
"degraded_restrictive_signal",
|
||||
False,
|
||||
),
|
||||
"degraded_for_s": round(degraded_for_s, 2),
|
||||
"critical_for_s": round(critical_for_s, 2),
|
||||
"healthy_for_s": round(healthy_for_s, 2),
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
motivo = estado.get("motivo", "Estado operacional inválido para movimento")
|
||||
debug["motivos"].append(motivo)
|
||||
|
||||
filtro_vel.reset(0.0)
|
||||
_resetar_rampa_velocidade(filtro_vel)
|
||||
return _montar_comando_retorno(
|
||||
velocidade_sp=0.0,
|
||||
frear=True,
|
||||
|
|
@ -135,6 +135,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
reduzir_para_pulverizar = False
|
||||
reduzir_para_fim_corredor = False
|
||||
reduzir_por_curva = False
|
||||
reduzir_por_ipb = False
|
||||
|
||||
if status_carro == StatusCarroMapa.Parado:
|
||||
velocidade_sp = 0.0
|
||||
|
|
@ -150,7 +151,6 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
]:
|
||||
velocidade_sp = vel_curva
|
||||
reduzir_por_curva = True
|
||||
resetar_filtro = True
|
||||
debug["motivos"].append("movimento em curva/manobra")
|
||||
|
||||
elif status_carro in [
|
||||
|
|
@ -188,13 +188,11 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
debug["weed"] = info_ervas
|
||||
|
||||
if reduzir_para_pulverizar:
|
||||
velocidade_sp = vel_com_ervas
|
||||
resetar_filtro = True
|
||||
velocidade_sp = min(velocidade_sp, vel_com_ervas)
|
||||
debug["motivos"].append("ervas no radar: reduzindo para pulverizar")
|
||||
|
||||
elif reduzir_para_fim_corredor:
|
||||
velocidade_sp = vel_com_ervas
|
||||
resetar_filtro = True
|
||||
velocidade_sp = min(velocidade_sp, vel_com_ervas)
|
||||
debug["motivos"].append("fim de corredor próximo: reduzindo")
|
||||
|
||||
else:
|
||||
|
|
@ -220,7 +218,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
|
||||
# Se já decidiu parar, não faz limitador tentar reviver velocidade.
|
||||
if hard_stop:
|
||||
filtro_vel.reset(0.0)
|
||||
_resetar_rampa_velocidade(filtro_vel)
|
||||
return _montar_comando_retorno(
|
||||
velocidade_sp=0.0,
|
||||
frear=frear,
|
||||
|
|
@ -239,7 +237,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
|
||||
if status_ipb not in [StatusModulo.OPERANTE, StatusModulo.ALERTA]:
|
||||
velocidade_sp = min(velocidade_sp, vel_degradada)
|
||||
resetar_filtro = True
|
||||
reduzir_por_ipb = True
|
||||
debug["limitadores"].append(
|
||||
f"IPB não operante: {status_ipb.name}, velocidade degradada"
|
||||
)
|
||||
|
|
@ -270,7 +268,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
debug["limitadores"].append("Visual Worker solicitou stop")
|
||||
|
||||
if hard_stop:
|
||||
filtro_vel.reset(0.0)
|
||||
_resetar_rampa_velocidade(filtro_vel)
|
||||
return _montar_comando_retorno(
|
||||
velocidade_sp=0.0,
|
||||
frear=frear,
|
||||
|
|
@ -300,7 +298,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
debug["limitadores"].append("IMU solicitou stop")
|
||||
|
||||
if hard_stop:
|
||||
filtro_vel.reset(0.0)
|
||||
_resetar_rampa_velocidade(filtro_vel)
|
||||
return _montar_comando_retorno(
|
||||
velocidade_sp=0.0,
|
||||
frear=frear,
|
||||
|
|
@ -329,16 +327,27 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
# Reduções por segurança/curva/pulverização entram imediatamente.
|
||||
# Aceleração normal é filtrada para não dar tranco.
|
||||
|
||||
reducao_seguranca = bool(
|
||||
reduzir_para_fim_corredor
|
||||
or reduzir_por_curva
|
||||
or reduzir_por_ipb
|
||||
or debug.get("visual", {}).get("mode") in ["caution", "slowdown"]
|
||||
or debug.get("imu", {}).get("risk_level_num", 0) >= 1
|
||||
)
|
||||
|
||||
if velocidade_sp <= 0.0:
|
||||
filtro_vel.reset(0.0)
|
||||
_resetar_rampa_velocidade(filtro_vel)
|
||||
velocidade_filtrada = 0.0
|
||||
|
||||
elif resetar_filtro or reduzir_para_pulverizar or reduzir_para_fim_corredor or reduzir_por_curva:
|
||||
filtro_vel.reset(velocidade_sp)
|
||||
velocidade_filtrada = velocidade_sp
|
||||
|
||||
else:
|
||||
velocidade_filtrada = filtro_vel.filtrar(velocidade_sp)
|
||||
velocidade_filtrada = _aplicar_rampa_velocidade(
|
||||
filtro_vel,
|
||||
velocidade_sp,
|
||||
hard_stop=False,
|
||||
frear=frear,
|
||||
reducao_seguranca=reducao_seguranca,
|
||||
mov_cfg=mov_cfg,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
velocidade_filtrada = round(
|
||||
_clamp(velocidade_filtrada, 0.0, vel_sem_ervas),
|
||||
|
|
@ -359,7 +368,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
|
|||
mostrar_log(f"Erro ao definir o comando de movimentacao: {e}")
|
||||
|
||||
try:
|
||||
filtro_vel.reset(0.0)
|
||||
_resetar_rampa_velocidade(filtro_vel)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -570,7 +579,7 @@ def _aplicar_limitador_visual(
|
|||
}.get(mode, 1.00)
|
||||
|
||||
vel_nova = float(velocidade_atual) * fator_mode
|
||||
resetar_filtro = fator_mode <= 0.75
|
||||
resetar_filtro = False
|
||||
stop = fator_mode <= 0.0
|
||||
|
||||
debug.update({
|
||||
|
|
@ -664,7 +673,7 @@ def _aplicar_limitador_imu(*, velocidade_atual):
|
|||
return velocidade_atual, False, False, debug
|
||||
|
||||
vel_nova = float(velocidade_atual) * velocidade_factor
|
||||
resetar_filtro = velocidade_factor <= 0.75
|
||||
resetar_filtro = False
|
||||
|
||||
return vel_nova, resetar_filtro, False, debug
|
||||
|
||||
|
|
@ -722,6 +731,105 @@ def calcular_velocidade_relativa(
|
|||
return round(_clamp(vel, vel_min, vel_max), 2)
|
||||
|
||||
|
||||
def _aplicar_rampa_velocidade(
|
||||
filtro_vel,
|
||||
velocidade_alvo,
|
||||
*,
|
||||
hard_stop=False,
|
||||
frear=False,
|
||||
reducao_seguranca=False,
|
||||
mov_cfg=None,
|
||||
debug=None
|
||||
):
|
||||
agora = time.monotonic()
|
||||
|
||||
if hard_stop or frear or velocidade_alvo <= 0.0:
|
||||
try:
|
||||
_resetar_rampa_velocidade(filtro_vel)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
filtro_vel._ultimo_sp_campo = 0.0
|
||||
filtro_vel._ultimo_ts_campo = agora
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return 0.0
|
||||
|
||||
mov_cfg = _dict(mov_cfg or {})
|
||||
|
||||
subida_pct_s = max(
|
||||
1.0,
|
||||
_float(mov_cfg.get("rampa_aceleracao_pct_s", 12.0), 12.0)
|
||||
)
|
||||
|
||||
descida_normal_pct_s = max(
|
||||
1.0,
|
||||
_float(mov_cfg.get("rampa_desaceleracao_pct_s", 18.0), 18.0)
|
||||
)
|
||||
|
||||
descida_seguranca_pct_s = max(
|
||||
descida_normal_pct_s,
|
||||
_float(mov_cfg.get("rampa_desaceleracao_seguranca_pct_s", 35.0), 35.0)
|
||||
)
|
||||
|
||||
try:
|
||||
ultimo = float(getattr(filtro_vel, "_ultimo_sp_campo", 0.0))
|
||||
ultimo_ts = float(getattr(filtro_vel, "_ultimo_ts_campo", 0.0))
|
||||
except Exception:
|
||||
ultimo = 0.0
|
||||
ultimo_ts = 0.0
|
||||
|
||||
if ultimo_ts <= 0.0:
|
||||
ultimo = 0.0
|
||||
ultimo_ts = agora
|
||||
try:
|
||||
filtro_vel._ultimo_sp_campo = ultimo
|
||||
filtro_vel._ultimo_ts_campo = ultimo_ts
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if debug is not None:
|
||||
debug["rampa_velocidade_init"] = "inicializada_em_zero"
|
||||
|
||||
return 0.0
|
||||
|
||||
dt = _clamp(agora - ultimo_ts, 0.02, 0.5)
|
||||
|
||||
delta = float(velocidade_alvo) - ultimo
|
||||
|
||||
if delta >= 0:
|
||||
max_delta = subida_pct_s * dt
|
||||
else:
|
||||
taxa_descida = descida_seguranca_pct_s if reducao_seguranca else descida_normal_pct_s
|
||||
max_delta = taxa_descida * dt
|
||||
|
||||
delta_limitado = _clamp(delta, -max_delta, max_delta)
|
||||
saida = ultimo + delta_limitado
|
||||
|
||||
try:
|
||||
filtro_vel._ultimo_sp_campo = saida
|
||||
filtro_vel._ultimo_ts_campo = agora
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if debug is not None:
|
||||
debug["rampa_velocidade"] = {
|
||||
"alvo": round(float(velocidade_alvo), 2),
|
||||
"saida": round(float(saida), 2),
|
||||
"ultimo": round(float(ultimo), 2),
|
||||
"dt": round(float(dt), 3),
|
||||
"delta": round(float(delta), 2),
|
||||
"delta_limitado": round(float(delta_limitado), 2),
|
||||
"subida_pct_s": subida_pct_s,
|
||||
"descida_normal_pct_s": descida_normal_pct_s,
|
||||
"descida_seguranca_pct_s": descida_seguranca_pct_s,
|
||||
"reducao_seguranca": bool(reducao_seguranca),
|
||||
}
|
||||
|
||||
return saida
|
||||
|
||||
# ============================================================
|
||||
# Retorno
|
||||
# ============================================================
|
||||
|
|
@ -738,6 +846,21 @@ def _montar_comando_retorno(velocidade_sp: float, frear: bool = False, debug: di
|
|||
# Helpers
|
||||
# ============================================================
|
||||
|
||||
def _resetar_rampa_velocidade(filtro_vel):
|
||||
agora = time.monotonic()
|
||||
|
||||
try:
|
||||
filtro_vel.reset(0.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
filtro_vel._ultimo_sp_campo = 0.0
|
||||
filtro_vel._ultimo_ts_campo = agora
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _status_modulo(t_code):
|
||||
modulo = _dict(ContextoGlobalRedis.get_modulo(t_code))
|
||||
saude = _dict(modulo.get("saude", {}))
|
||||
|
|
|
|||
|
|
@ -180,6 +180,9 @@ class CameraManager:
|
|||
"campos": 0,
|
||||
}
|
||||
|
||||
self._ultimo_frame_preview_por_tipo = {}
|
||||
self._ultimo_frame_preview_ts_por_tipo = {}
|
||||
|
||||
def inicializar(self, mx_id):
|
||||
if self.iniciando:
|
||||
return
|
||||
|
|
@ -826,13 +829,19 @@ class CameraManager:
|
|||
# ============================================================
|
||||
|
||||
def get_selected_frame(self, frame_type: TipoFrameCamera):
|
||||
if not self._visual_disponivel_para_frame():
|
||||
return None
|
||||
|
||||
try:
|
||||
try:
|
||||
tipo_enum = TipoFrameCamera(frame_type)
|
||||
except Exception:
|
||||
tipo_enum = frame_type
|
||||
|
||||
# Se visual não está pronto, ainda tenta devolver último frame válido.
|
||||
if not self._visual_disponivel_para_frame():
|
||||
return self._get_cached_preview_frame(tipo_enum)
|
||||
|
||||
with self._cache_lock:
|
||||
rgb = self._ultimo_rgb_frame
|
||||
depth = self._ultimo_depth_frame
|
||||
rgb = self._copiar_frame(self._ultimo_rgb_frame)
|
||||
depth = self._copiar_frame(self._ultimo_depth_frame)
|
||||
pred = self._ultimo_predictions
|
||||
dets = list(self._ultimo_detections or [])
|
||||
snapshot = self._ultimo_snapshot
|
||||
|
|
@ -840,15 +849,15 @@ class CameraManager:
|
|||
params = getattr(self.camera, "parametros", {}) if self.camera is not None else {}
|
||||
|
||||
# PosProcessamento no Visual Worker = imagem RGB original em melhor qualidade.
|
||||
# Aqui não renderiza overlay, segmentação ou debug.
|
||||
if frame_type == TipoFrameCamera.PosProcessamento:
|
||||
if rgb is None or not hasattr(rgb, "size") or rgb.size <= 0:
|
||||
return None
|
||||
if tipo_enum == TipoFrameCamera.PosProcessamento:
|
||||
if self._frame_valido(rgb):
|
||||
self._set_cached_preview_frame(tipo_enum, rgb)
|
||||
return self._copiar_frame(rgb)
|
||||
|
||||
return rgb.copy()
|
||||
return self._get_cached_preview_frame(tipo_enum)
|
||||
|
||||
return self.renderer.get_selected_frame(
|
||||
frame_type=frame_type,
|
||||
frame = self.renderer.get_selected_frame(
|
||||
frame_type=tipo_enum,
|
||||
rgb_frame=rgb,
|
||||
pred_ids=pred,
|
||||
depth_frame=depth,
|
||||
|
|
@ -858,10 +867,102 @@ class CameraManager:
|
|||
alpha=0.50,
|
||||
)
|
||||
|
||||
if self._frame_valido(frame):
|
||||
self._set_cached_preview_frame(tipo_enum, frame)
|
||||
return self._copiar_frame(frame)
|
||||
|
||||
# Se o renderer falhou ou faltou algum insumo naquele instante,
|
||||
# devolve o último frame válido desse mesmo tipo.
|
||||
cached = self._get_cached_preview_frame(tipo_enum)
|
||||
if self._frame_valido(cached):
|
||||
return cached
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[visual] erro em get_selected_frame({frame_type}): {e}")
|
||||
|
||||
try:
|
||||
return self._get_cached_preview_frame(frame_type)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _frame_valido(self, frame) -> bool:
|
||||
return (
|
||||
frame is not None and
|
||||
hasattr(frame, "size") and
|
||||
frame.size > 0
|
||||
)
|
||||
|
||||
def _copiar_frame(self, frame):
|
||||
if not self._frame_valido(frame):
|
||||
return None
|
||||
|
||||
try:
|
||||
return frame.copy()
|
||||
except Exception:
|
||||
return frame
|
||||
|
||||
def _frame_cache_key(self, frame_type):
|
||||
try:
|
||||
return TipoFrameCamera(frame_type).name
|
||||
except Exception:
|
||||
return str(frame_type)
|
||||
|
||||
def _set_cached_preview_frame(self, frame_type, frame, ts=None):
|
||||
if not self._frame_valido(frame):
|
||||
return False
|
||||
|
||||
key = self._frame_cache_key(frame_type)
|
||||
ts = time.time() if ts is None else ts
|
||||
frame_copy = self._copiar_frame(frame)
|
||||
|
||||
if not self._frame_valido(frame_copy):
|
||||
return False
|
||||
|
||||
with self._cache_lock:
|
||||
self._ultimo_frame_preview_por_tipo[key] = frame_copy
|
||||
self._ultimo_frame_preview_ts_por_tipo[key] = ts
|
||||
|
||||
return True
|
||||
|
||||
def _get_cached_preview_frame(self, frame_type):
|
||||
key = self._frame_cache_key(frame_type)
|
||||
|
||||
with self._cache_lock:
|
||||
frame = self._ultimo_frame_preview_por_tipo.get(key)
|
||||
|
||||
return self._copiar_frame(frame)
|
||||
|
||||
def _normalizar_frame_para_uint8(self, frame):
|
||||
"""
|
||||
Converte frame float/uint16/etc para uint8 apenas para replay visual.
|
||||
Útil para Heatmap/MatrizCusto caso algum renderer devolva matriz não-uint8.
|
||||
"""
|
||||
|
||||
try:
|
||||
if frame is None:
|
||||
return None
|
||||
|
||||
if getattr(frame, "dtype", None) == np.uint8:
|
||||
return frame
|
||||
|
||||
arr = frame.astype("float32")
|
||||
|
||||
min_v = float(arr.min())
|
||||
max_v = float(arr.max())
|
||||
|
||||
if max_v <= min_v:
|
||||
return np.zeros(arr.shape, dtype=np.uint8)
|
||||
|
||||
arr = (arr - min_v) / (max_v - min_v)
|
||||
arr = np.clip(arr * 255.0, 0, 255).astype("uint8")
|
||||
|
||||
return arr
|
||||
|
||||
except Exception:
|
||||
return frame
|
||||
|
||||
# ============================================================
|
||||
# Loops
|
||||
# ============================================================
|
||||
|
|
@ -1487,9 +1588,17 @@ class CameraManager:
|
|||
|
||||
frame = self.get_selected_frame(tipo_enum)
|
||||
|
||||
if frame is None or not hasattr(frame, "size") or frame.size <= 0:
|
||||
if not self._frame_valido(frame):
|
||||
self.mostrar_log(
|
||||
f"⚠️ Frame visual não salvo | "
|
||||
f"tipo={tipo_enum.name} "
|
||||
f"nome={frame_name}_{tipo_enum.name} "
|
||||
f"motivo=sem_frame_valido_em_cache"
|
||||
)
|
||||
continue
|
||||
|
||||
frame = self._copiar_frame(frame)
|
||||
|
||||
nome_base = f"{frame_name}_{tipo_enum.name}"
|
||||
|
||||
# ====================================================
|
||||
|
|
@ -1517,13 +1626,14 @@ class CameraManager:
|
|||
caminho = os.path.join(pasta_async, f"{nome_base_async}.png")
|
||||
|
||||
ok = cv2.imwrite(caminho, frame_async)
|
||||
ok_final = bool(ok and os.path.exists(caminho))
|
||||
|
||||
t_pp1 = time.perf_counter()
|
||||
|
||||
self.mostrar_log(
|
||||
f"[visual][SAVE_PP] salvo async | "
|
||||
f"nome={nome_base_async} "
|
||||
f"ok={ok} "
|
||||
f"ok={ok_final} "
|
||||
f"tempo_ms={(t_pp1 - t_pp0) * 1000.0:.1f}"
|
||||
)
|
||||
|
||||
|
|
@ -1548,14 +1658,26 @@ class CameraManager:
|
|||
# Salva JPEG leve para não entupir disco durante operação.
|
||||
caminho = os.path.join(pasta, f"{nome_base}.jpg")
|
||||
|
||||
if getattr(frame, "dtype", None) != np.uint8:
|
||||
frame_salvar = self._normalizar_frame_para_uint8(frame)
|
||||
else:
|
||||
frame_salvar = frame
|
||||
|
||||
ok = cv2.imwrite(
|
||||
caminho,
|
||||
frame,
|
||||
frame_salvar,
|
||||
[int(cv2.IMWRITE_JPEG_QUALITY), int(replay_jpeg_quality)]
|
||||
)
|
||||
|
||||
if ok:
|
||||
if ok and os.path.exists(caminho):
|
||||
frames_salvos.append(caminho)
|
||||
else:
|
||||
self.mostrar_log(
|
||||
f"❌ Falha ao salvar frame visual | "
|
||||
f"tipo={tipo_enum.name} "
|
||||
f"nome={nome_base} "
|
||||
f"caminho={caminho}"
|
||||
)
|
||||
|
||||
return frames_salvos
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class CameraManager:
|
|||
self._lock_tensor = threading.Lock()
|
||||
self._pred_lock = threading.RLock()
|
||||
self._pub_lock = threading.RLock()
|
||||
self._preview_lock = threading.RLock()
|
||||
|
||||
self.perf = VisualPerfMonitor(janela=180)
|
||||
|
||||
|
|
@ -131,6 +132,11 @@ class CameraManager:
|
|||
self._ultimo_preview_overlay = None
|
||||
self._ultimo_preview_debug = None
|
||||
|
||||
self._ultimo_preview_rgb_ts = 0.0
|
||||
self._ultimo_preview_seg_ts = 0.0
|
||||
self._ultimo_preview_overlay_ts = 0.0
|
||||
self._ultimo_preview_debug_ts = 0.0
|
||||
|
||||
self._fps_infer_last_ts = None
|
||||
self._fps_infer_ema = 0.0
|
||||
self._ultimo_infer_ms = 0.0
|
||||
|
|
@ -612,9 +618,11 @@ class CameraManager:
|
|||
if _frame_type not in self.FRAME_TYPES_PREVIEW:
|
||||
return None
|
||||
|
||||
# Se ainda não tem runtime/tensor, tenta devolver o último preview válido.
|
||||
if self.model_svc is None or self._ultimo_raw_input is None:
|
||||
return None
|
||||
return self._get_cached_preview_frame(_frame_type)
|
||||
|
||||
# Janela curta: usa cache recente.
|
||||
if (agora - self._ultimo_preview_ts) < 0.20:
|
||||
cached = self._get_cached_preview_frame(_frame_type)
|
||||
if cached is not None:
|
||||
|
|
@ -633,28 +641,44 @@ class CameraManager:
|
|||
overlay_bgr=overlay_frame,
|
||||
)
|
||||
|
||||
# Atualiza o timestamp da tentativa de preview.
|
||||
self._ultimo_preview_ts = agora
|
||||
self._ultimo_preview_rgb = rgb_frame
|
||||
self._ultimo_preview_seg = seg_frame
|
||||
self._ultimo_preview_overlay = overlay_frame
|
||||
self._ultimo_preview_debug = debug_frame
|
||||
|
||||
# Importante:
|
||||
# só atualiza cache se o frame novo for válido.
|
||||
# Nunca apaga último válido com None.
|
||||
self._set_cached_preview_frame(TipoFrameCamera.Rgb, rgb_frame, agora)
|
||||
self._set_cached_preview_frame(TipoFrameCamera.Segmentacao, seg_frame, agora)
|
||||
self._set_cached_preview_frame(TipoFrameCamera.Overlay, overlay_frame, agora)
|
||||
self._set_cached_preview_frame(TipoFrameCamera.Debug, debug_frame, agora)
|
||||
|
||||
# Retorna o frame pedido.
|
||||
# Se o novo veio None, devolve o último válido cacheado.
|
||||
return self._get_cached_preview_frame(_frame_type)
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed] erro em get_selected_frame: {e}")
|
||||
return None
|
||||
|
||||
# Mesmo em erro, tenta devolver último válido.
|
||||
try:
|
||||
return self._get_cached_preview_frame(_frame_type)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _get_cached_preview_frame(self, frame_type):
|
||||
if frame_type == TipoFrameCamera.Rgb:
|
||||
return self._ultimo_preview_rgb
|
||||
if frame_type == TipoFrameCamera.Segmentacao:
|
||||
return self._ultimo_preview_seg
|
||||
if frame_type == TipoFrameCamera.Overlay:
|
||||
return self._ultimo_preview_overlay
|
||||
if frame_type == TipoFrameCamera.Debug:
|
||||
return self._ultimo_preview_debug
|
||||
return None
|
||||
with self._preview_lock:
|
||||
if frame_type == TipoFrameCamera.Rgb:
|
||||
frame = self._ultimo_preview_rgb
|
||||
elif frame_type == TipoFrameCamera.Segmentacao:
|
||||
frame = self._ultimo_preview_seg
|
||||
elif frame_type == TipoFrameCamera.Overlay:
|
||||
frame = self._ultimo_preview_overlay
|
||||
elif frame_type == TipoFrameCamera.Debug:
|
||||
frame = self._ultimo_preview_debug
|
||||
else:
|
||||
frame = None
|
||||
|
||||
return self._copiar_frame(frame)
|
||||
|
||||
def get_debug_frame(self, mostrar=False, overlay_bgr=None):
|
||||
overlay = overlay_bgr if overlay_bgr is not None else self._ultimo_preview_overlay
|
||||
|
|
@ -810,6 +834,55 @@ class CameraManager:
|
|||
self.mostrar_log(f"Erro ao montar debug overlay weed: {e}")
|
||||
return None
|
||||
|
||||
def _frame_valido(self, frame) -> bool:
|
||||
return (
|
||||
frame is not None and
|
||||
hasattr(frame, "size") and
|
||||
frame.size > 0
|
||||
)
|
||||
|
||||
def _copiar_frame(self, frame):
|
||||
if not self._frame_valido(frame):
|
||||
return None
|
||||
|
||||
try:
|
||||
return frame.copy()
|
||||
except Exception:
|
||||
return frame
|
||||
|
||||
def _set_cached_preview_frame(self, frame_type, frame, ts=None):
|
||||
if not self._frame_valido(frame):
|
||||
return False
|
||||
|
||||
ts = time.time() if ts is None else ts
|
||||
frame_copy = self._copiar_frame(frame)
|
||||
|
||||
if not self._frame_valido(frame_copy):
|
||||
return False
|
||||
|
||||
with self._preview_lock:
|
||||
if frame_type == TipoFrameCamera.Rgb:
|
||||
self._ultimo_preview_rgb = frame_copy
|
||||
self._ultimo_preview_rgb_ts = ts
|
||||
return True
|
||||
|
||||
if frame_type == TipoFrameCamera.Segmentacao:
|
||||
self._ultimo_preview_seg = frame_copy
|
||||
self._ultimo_preview_seg_ts = ts
|
||||
return True
|
||||
|
||||
if frame_type == TipoFrameCamera.Overlay:
|
||||
self._ultimo_preview_overlay = frame_copy
|
||||
self._ultimo_preview_overlay_ts = ts
|
||||
return True
|
||||
|
||||
if frame_type == TipoFrameCamera.Debug:
|
||||
self._ultimo_preview_debug = frame_copy
|
||||
self._ultimo_preview_debug_ts = ts
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# ============================================================
|
||||
# Loops
|
||||
# ============================================================
|
||||
|
|
@ -1989,13 +2062,18 @@ class CameraManager:
|
|||
# ====================================================
|
||||
# 2) FLUXO ATUAL: imagens para replay
|
||||
# ====================================================
|
||||
frame = self.get_selected_frame(frame_type)
|
||||
frame = self.get_selected_frame(tipo_enum)
|
||||
|
||||
if frame is None:
|
||||
if not self._frame_valido(frame):
|
||||
self.mostrar_log(
|
||||
f"⚠️ Frame não salvo | "
|
||||
f"tipo={tipo_nome} "
|
||||
f"nome={nome_frame} "
|
||||
f"motivo=sem_frame_valido_em_cache"
|
||||
)
|
||||
continue
|
||||
|
||||
if not hasattr(frame, "size") or frame.size <= 0:
|
||||
continue
|
||||
frame = self._copiar_frame(frame)
|
||||
|
||||
caminho = os.path.join(pasta, f"{nome_frame}.jpg")
|
||||
|
||||
|
|
@ -2006,11 +2084,16 @@ class CameraManager:
|
|||
else:
|
||||
frame_salvar = frame
|
||||
|
||||
cv2.imwrite(
|
||||
caminho,
|
||||
frame_salvar,
|
||||
[int(cv2.IMWRITE_JPEG_QUALITY), 70]
|
||||
)
|
||||
ok = cv2.imwrite(caminho, frame_salvar, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
|
||||
|
||||
if not ok or not os.path.exists(caminho):
|
||||
self.mostrar_log(
|
||||
f"❌ Falha ao salvar frame | "
|
||||
f"tipo={tipo_nome} "
|
||||
f"nome={nome_frame} "
|
||||
f"caminho={caminho}"
|
||||
)
|
||||
continue
|
||||
|
||||
frames_salvos.append(caminho)
|
||||
|
||||
|
|
|
|||
|
|
@ -233,14 +233,14 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
|
|||
};
|
||||
|
||||
double PotenciaBase22Psi[8] = {
|
||||
0.0,
|
||||
35.0,
|
||||
56.4,
|
||||
62.5,
|
||||
65.8,
|
||||
71.7,
|
||||
77.5,
|
||||
100.0
|
||||
0.0, // 0 bicos
|
||||
35.0, // 1 bico
|
||||
56.0, // 2 bicos
|
||||
63.0, // 3 bicos
|
||||
68.0, // 4 bicos
|
||||
82.0, // 5 bicos
|
||||
88.0, // 6 bicos
|
||||
100.0 // 7 bicos
|
||||
};
|
||||
|
||||
double PressaoReferenciaBaixaPsi = 15.0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue