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:
Diego Freitas 2026-07-06 16:50:48 -03:00
parent 66ad55aeb3
commit bdfa707968
11 changed files with 796 additions and 181 deletions

View File

@ -123,7 +123,7 @@ namespace AgroBase.Models.Modules
[JsonIgnore] [JsonIgnore]
private SensorModel SensorFluxoLinha => Sensores.FirstOrDefault(x => x.Componente == S_Code.sFLX && x.ID == "FLXLN"); 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; public double VazaoMaximaSensor_LMin { get; set; } = 20.0;

View File

@ -139,6 +139,21 @@ namespace AgroBase.Models.Modules
public Direcao UltimaDirecao { get; set; } = Direcao.Parado; public Direcao UltimaDirecao { get; set; } = Direcao.Parado;
public DateTime UltimoComandoEnviado { get; set; } = DateTime.MinValue; 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 public bool RetornandoAzero
{ {
get get
@ -152,17 +167,14 @@ namespace AgroBase.Models.Modules
{ {
get get
{ {
return RPM != 0; return Math.Abs(RPM) > 1;
} }
} }
public bool AtingiuAnguloSP public bool AtingiuAnguloSP
{ {
get get
{ {
double anguloSp = return FuncoesMatematicas.ValorEstaEntre(AnguloLeitura, AnguloSPAtual, 2);
!Comandar || Sentido_SP == Sentido.Parado ? 0 :
Angulo_SP;
return FuncoesMatematicas.ValorEstaEntre(AnguloLeitura, anguloSp, 1);
} }
} }

View File

@ -518,7 +518,7 @@ namespace AgroBase.Models
AtuPercentualErvasBicoOff = 2, AtuPercentualErvasBicoOff = 2,
AtuPercentualErvasBicoOn = 1, AtuPercentualErvasBicoOn = 1,
AtuPercentualInicioPulverizacao = 85, AtuPercentualInicioPulverizacao = 85,
AtuPressaoLinha = 22, AtuPressaoLinha = 18,
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao, AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
AtuModoControle = ModoControleBomba.PID AtuModoControle = ModoControleBomba.PID
}; };
@ -629,7 +629,7 @@ namespace AgroBase.Models
AtuPercentualErvasBicoOff = 2, AtuPercentualErvasBicoOff = 2,
AtuPercentualErvasBicoOn = 1, AtuPercentualErvasBicoOn = 1,
AtuPercentualInicioPulverizacao = 85, AtuPercentualInicioPulverizacao = 85,
AtuPressaoLinha = 22, AtuPressaoLinha = 18,
AtuAgitadorModo = ModoAgitadorCalda.Continuo, AtuAgitadorModo = ModoAgitadorCalda.Continuo,
AtuModoControle = ModoControleBomba.PID, AtuModoControle = ModoControleBomba.PID,
}; };
@ -768,8 +768,8 @@ namespace AgroBase.Models
AtuDuracaoAtuacao = 200, AtuDuracaoAtuacao = 200,
AtuPercentualErvasBicoOff = 2, AtuPercentualErvasBicoOff = 2,
AtuPercentualErvasBicoOn = 1, AtuPercentualErvasBicoOn = 1,
AtuPercentualInicioPulverizacao = 70, AtuPercentualInicioPulverizacao = 85,
AtuPressaoLinha = 22, AtuPressaoLinha = 18,
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao, AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
AtuModoControle = ModoControleBomba.PID, AtuModoControle = ModoControleBomba.PID,
}; };
@ -908,7 +908,7 @@ namespace AgroBase.Models
AtuPercentualErvasBicoOff = 2, AtuPercentualErvasBicoOff = 2,
AtuPercentualErvasBicoOn = 1, AtuPercentualErvasBicoOn = 1,
AtuPercentualInicioPulverizacao = 85, AtuPercentualInicioPulverizacao = 85,
AtuPressaoLinha = 22, AtuPressaoLinha = 18,
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao, AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
AtuModoControle = ModoControleBomba.PID, AtuModoControle = ModoControleBomba.PID,
}; };
@ -1007,7 +1007,7 @@ namespace AgroBase.Models
AtuPercentualErvasBicoOff = 2, AtuPercentualErvasBicoOff = 2,
AtuPercentualErvasBicoOn = 1, AtuPercentualErvasBicoOn = 1,
AtuPercentualInicioPulverizacao = 85, AtuPercentualInicioPulverizacao = 85,
AtuPressaoLinha = 22, AtuPressaoLinha = 18,
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao, AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
AtuModoControle = ModoControleBomba.PID, AtuModoControle = ModoControleBomba.PID,
}; };

View File

@ -95,12 +95,12 @@ namespace AgroBase.Services
*/ */
{ MksFuncCode.PulsosRecebidos, TimeSpan.FromMilliseconds(70) }, { MksFuncCode.PulsosRecebidos, TimeSpan.FromMilliseconds(70) },
{ MksFuncCode.IO, TimeSpan.FromMilliseconds(700) }, { MksFuncCode.IO, TimeSpan.FromMilliseconds(700) },
{ MksFuncCode.RPM, TimeSpan.FromMilliseconds(500) },
/* /*
* Diagnóstico opcional. * Diagnóstico opcional.
* Não entram em OrdemPolling por padrão. * Não entram em OrdemPolling por padrão.
*/ */
{ MksFuncCode.RPM, TimeSpan.FromMilliseconds(500) },
{ MksFuncCode.ErroAcumulado, TimeSpan.FromSeconds(1) }, { MksFuncCode.ErroAcumulado, TimeSpan.FromSeconds(1) },
{ MksFuncCode.EncoderAcumulado, TimeSpan.FromSeconds(1) }, { MksFuncCode.EncoderAcumulado, TimeSpan.FromSeconds(1) },
{ MksFuncCode.EncoderCarregado, TimeSpan.FromSeconds(2) }, { MksFuncCode.EncoderCarregado, TimeSpan.FromSeconds(2) },
@ -965,6 +965,15 @@ namespace AgroBase.Services
return (ushort)((data[startIndex] << 8) | data[startIndex + 1]); 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) private static int DecodeInt32BE(byte[] data, int startIndex)
{ {
if (data == null || data.Length < startIndex + 4) if (data == null || data.Length < startIndex + 4)
@ -1213,7 +1222,9 @@ namespace AgroBase.Services
if (data == null || data.Length < 3) if (data == null || data.Length < 3)
return; return;
item.RPM.valor = DecodeUInt16BE(data, 1); short rpm = DecodeInt16BE(data, 1);
item.RPM.valor = rpm;
} }
private void ProcessarPulsosRecebidos(MKS057DModel item, byte[] data) private void ProcessarPulsosRecebidos(MKS057DModel item, byte[] data)

View File

@ -308,6 +308,9 @@ namespace AgroBase.Services.Operadores
{ {
percent_vel_max = pControle.MovVelocidadeSErvasPercent, percent_vel_max = pControle.MovVelocidadeSErvasPercent,
percent_vel_min = pControle.MovVelocidadeCErvasPercent, 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 ("Dir", new

View File

@ -36,6 +36,7 @@ class ModuloDirecional(ModuloDiagnosticoBase):
self.t_code = T_Code.Dir self.t_code = T_Code.Dir
self.nome = "Direcional" self.nome = "Direcional"
self.timeout = 3 self.timeout = 3
self._estado_driver = {}
def atualizar_saude(self): def atualizar_saude(self):
try: try:
@ -179,8 +180,8 @@ class ModuloDirecional(ModuloDiagnosticoBase):
leitura = self._avaliar_resposta_recente(modulo, motivos, condicoes) leitura = self._avaliar_resposta_recente(modulo, motivos, condicoes)
feedback = self._avaliar_feedback(modulo, motivos, condicoes, referenciando) feedback = self._avaliar_feedback(modulo, motivos, condicoes, referenciando)
comando = self._avaliar_seguimento_comando(modulo, motivos, condicoes) comando = self._avaliar_seguimento_comando(endereco_str, modulo, motivos, condicoes)
coerencia = self._avaliar_coerencia_operacional(modulo, motivos, condicoes) coerencia = self._avaliar_coerencia_operacional(endereco_str, modulo, motivos, condicoes)
telemetria = self._avaliar_telemetria(modulo, motivos, referenciando) telemetria = self._avaliar_telemetria(modulo, motivos, referenciando)
componentes = { componentes = {
@ -336,62 +337,116 @@ class ModuloDirecional(ModuloDiagnosticoBase):
"angulo_sp": angulo_sp, "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)) comandar = self._bool(modulo.get("comandar", True))
atingiu = self._bool(modulo.get("atingiu_angulo_sp", False)) atingiu = self._bool(modulo.get("atingiu_angulo_sp", False))
retornando_a_zero = self._bool(modulo.get("retornando_a_zero", 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( angulo = self._float(modulo.get("angulo", 0.0), 0.0)
modulo.get( angulo_sp = self._float(modulo.get("angulo_sp", 0.0), 0.0)
"erro_angulo", erro_angulo = abs(angulo_sp - angulo)
abs(
self._float(modulo.get("angulo", 0.0), 0.0) -
self._float(modulo.get("angulo_sp", 0.0), 0.0)
)
),
0.0,
)
ultimo_comando_ms = self._float(modulo.get("ultimo_comando_ms", 999999), 999999) ultimo_comando_ms = self._float(
ultima_direcao = self._safe_int( modulo.get("ultimo_comando_ms", 999999),
modulo.get("ultima_direcao", self._direcao_parada_value()), 999999
self._direcao_parada_value(),
) )
if not comandar: if not comandar:
# Driver conectado, mas não participa do comando atual.
return { return {
"score": 100, "score": 100,
"falha_seguimento": False, "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 { return {
"score": 100, "score": 100,
"falha_seguimento": False, "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 { return {
"score": 96, "score": 96,
"falha_seguimento": False, "falha_seguimento": False,
} }
if retornando_a_zero and erro_angulo <= self.ERRO_ALERTA_GRAUS: # Ainda longe, mas mexendo ou melhorando.
motivos.append( if ultimo_comando_ms < alerta_ms and (em_movimento or erro_diminuindo):
f"Retornando a zero com erro residual ({erro_angulo:.2f}°)"
)
return { return {
"score": 88, "score": 92,
"falha_seguimento": False, "falha_seguimento": False,
} }
# Aqui começa a diferença entre telemetria lenta e comando que não pegou. # Alerta real: demorou e ainda está longe, mas não é falha dura.
if ultimo_comando_ms >= self.COMANDO_FALHA_MS and erro_angulo >= self.ERRO_FALHA_GRAUS: 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( motivos.append(
f"Comando direcional não convergiu " f"Comando direcional não convergiu "
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)" f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
) )
condicoes.append({ condicoes.append({
"valor": erro_angulo, "valor": erro_angulo,
"severidade": 92, "severidade": 92,
@ -400,55 +455,44 @@ class ModuloDirecional(ModuloDiagnosticoBase):
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)" f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
), ),
}) })
return { return {
"score": 20, "score": 20,
"falha_seguimento": True, "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( motivos.append(
f"Ângulo ainda distante do SP " f"Retornando a zero com erro residual ({erro_angulo:.2f}°)"
f"(erro={erro_angulo:.2f}°, comando há {ultimo_comando_ms:.0f} ms)"
) )
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 { return {
"score": 90, "score": 88,
"falha_seguimento": False, "falha_seguimento": False,
} }
return { return {
"score": 100, "score": 90,
"falha_seguimento": False, "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)) referenciado = self._bool(modulo.get("referenciado", False))
referenciando = self._bool(modulo.get("referenciando", 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( sentido_real = self._safe_int(
modulo.get("sentido_real", self._sentido_parado_value()), modulo.get("sentido_real", self._sentido_parado_value()),
self._sentido_parado_value(), self._sentido_parado_value(),
) )
sentido_sp = self._safe_int( sentido_sp = self._safe_int(
modulo.get("sentido_sp", self._sentido_parado_value()), modulo.get("sentido_sp", self._sentido_parado_value()),
self._sentido_parado_value(), self._sentido_parado_value(),
@ -466,24 +510,46 @@ class ModuloDirecional(ModuloDiagnosticoBase):
}) })
score = min(score, 75) score = min(score, 75)
# Sentido real vem do sensor do rasgo. # Durante movimento/transição, não julga sentido real.
# Como esse sensor pode ter borda/transição, não vamos bloquear operação por isso sozinho. if em_movimento or not atingiu or erro_angulo > 3.0:
if sentido != self._sentido_parado_value() and sentido != sentido_real: st["incoerencia_sentido_count"] = 0
motivos.append( return {
f"Sentido real incoerente " "score": score,
f"(real={self._nome_sentido(sentido_real)}, esperado={self._nome_sentido(sentido)})" "falha_coerencia": False,
) }
condicoes.append({
"valor": sentido_real, # Perto de zero, o sentido/lado não é confiável.
"severidade": 70, if abs(angulo_sp) < 2.0:
"descricao": ( st["incoerencia_sentido_count"] = 0
"Sentido real difere do sentido calculado " return {
f"(real={self._nome_sentido(sentido_real)}, " "score": score,
f"esperado={self._nome_sentido(sentido)}, " "falha_coerencia": False,
f"SP={self._nome_sentido(sentido_sp)})" }
),
}) # Só compara quando está parado/estável no SP.
score = min(score, 70) 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 { return {
"score": score, "score": score,
@ -812,3 +878,14 @@ class ModuloDirecional(ModuloDiagnosticoBase):
return Sentido(valor).name return Sentido(valor).name
except Exception: except Exception:
return str(valor) 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]

View File

@ -29,7 +29,8 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
nunca pela frequência do loop do Health Worker. 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. 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. 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. consumidores existentes.
""" """
@ -1123,30 +1124,66 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
math.isfinite(hb_age) and hb_age >= self._heartbeat_critical_s 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 # Perda percentual só vira evidência após janela mínima e pelo menos
# menos duas falhas reais. Isso impede que 1 falha isolada durante o # duas falhas reais. Uma falha isolada não degrada o enlace.
# aquecimento apareça como 16,7% e dispare degradação.
loss_degraded = ( loss_degraded = (
metrics["probe_sample_count"] >= 12 metrics["probe_sample_count"] >= 12
and metrics["failures_s"] >= 2 and metrics["failures_s"] >= 2
and loss_s >= 10.0 and loss_s >= 10.0
) )
qos_degraded = probe_ready and ( # Jitter sozinho vira aviso operacional, não prova de perda de enlace.
loss_degraded # No teste de 03/07, havia jitter alto, mas perda 0%, RTT baixo,
or (rtt_s >= 180.0 and jitter_s >= 50.0) # heartbeat ok e ping recente. Esse cenário deve reduzir tráfego,
or jitter_s >= 100.0 # não travar avanço.
or self._consecutive_ping_failures >= 2 jitter_warning = bool(
probe_ready
and jitter_s >= 80.0
) )
corroborated_service_delay = heartbeat_delayed and ( jitter_isolated = bool(
loss_s >= 5.0 jitter_warning
or rtt_s >= 150.0 and loss_s <= 2.0
or jitter_s >= 60.0 and rtt_s <= 120.0
or self._consecutive_ping_failures >= 2 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 metrics["bw_util_s"] >= 85.0
and ( and (
loss_s >= 5.0 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( down_candidate = bool(
not evidence["route_ok"] not evidence["route_ok"]
or ( 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 { return {
"degraded_now": degraded_now, "degraded_now": degraded_now,
"critical_now": critical_now, "critical_now": critical_now,
@ -1202,9 +1254,12 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
"heartbeat_very_delayed": heartbeat_very_delayed, "heartbeat_very_delayed": heartbeat_very_delayed,
"bandwidth_corroborated": bandwidth_corroborated, "bandwidth_corroborated": bandwidth_corroborated,
"qos_degraded": qos_degraded, "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: 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 return max(0.0, now_mono - started_mono) if started_mono > 0 else 0.0
@ -1300,7 +1355,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
else: else:
bw_effective_score = max(85.0, metrics["bw_score_raw"]) 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"]: if evidence["strong_positive"]:
reachability_score = 100.0 reachability_score = 100.0
elif evidence["positive_count"] >= 2: elif evidence["positive_count"] >= 2:
@ -1308,7 +1363,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
elif evidence["any_positive"]: elif evidence["any_positive"]:
reachability_score = 80.0 reachability_score = 80.0
elif not metrics["probe_ready"] and evidence["route_ok"]: elif not metrics["probe_ready"] and evidence["route_ok"]:
reachability_score = 75.0 # aquecimento reachability_score = 75.0
elif evidence["route_ok"]: elif evidence["route_ok"]:
reachability_score = 30.0 reachability_score = 30.0
else: else:
@ -1323,8 +1378,6 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
+ 0.03 * bw_effective_score + 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 service_penalty = 0.0
if not heartbeat_snapshot["mqtt_connected"]: if not heartbeat_snapshot["mqtt_connected"]:
service_penalty += 4.0 service_penalty += 4.0
@ -1336,7 +1389,13 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
if self.link_state == self.LINK_CRITICAL: if self.link_state == self.LINK_CRITICAL:
health = min(health, 45.0) health = min(health, 45.0)
elif self.link_state == self.LINK_DEGRADED: 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 { return {
"health": round(health, 1), "health": round(health, 1),
@ -1463,6 +1522,76 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
self._max_distancia_desde_degradado = 0.0 self._max_distancia_desde_degradado = 0.0
self._max_distancia_ok = max(self._max_distancia_ok, distancia_base) 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 # PAYLOAD / APRESENTAÇÃO
# ====================================================================== # ======================================================================
@ -1518,6 +1647,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
down_confirmed, down_confirmed,
regra_distancia_ativa, regra_distancia_ativa,
distancia_base, distancia_base,
decisao_operacional,
): ):
motivos = [] motivos = []
condicoes = [] condicoes = []
@ -1636,6 +1766,31 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
condicoes.append(condition) condicoes.append(condition)
jitter_cond.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( self._append_individual(
individuais, individuais,
"ip_jitter", "ip_jitter",
@ -1789,6 +1944,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
"acoes": [ "acoes": [
"Reduzir tráfego não crítico", "Reduzir tráfego não crítico",
"Priorizar controle, RTCM e heartbeat", "Priorizar controle, RTCM e heartbeat",
"Permitir avanço restrito se controle remoto estiver estável",
"Permitir retorno ou manutenção de posição", "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 regra_distancia_ativa
and self.link_state == self.LINK_DEGRADED 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 self._distancia_inicio_degradado is not None
and distancia_base is not None and distancia_base is not None
and distancia_base > self._distancia_inicio_degradado + 5.0 and distancia_base > self._distancia_inicio_degradado + 5.0
@ -1929,6 +2090,15 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
else: else:
status = StatusModulo.OPERANTE 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( motivos, condicoes, saude_individual, risk_score = self._build_messages(
metrics, metrics,
heartbeat_snapshot, heartbeat_snapshot,
@ -1938,6 +2108,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
down_confirmed, down_confirmed,
regra_distancia_ativa, regra_distancia_ativa,
distancia_base, distancia_base,
decisao_operacional,
) )
degraded_for_s = self._elapsed_since(now_mono, self._degraded_since_mono) 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_degraded": self._cycles_degraded,
"cycles_critical": self._cycles_critical, "cycles_critical": self._cycles_critical,
"cycles_recovered": self._cycles_recovered, "cycles_recovered": self._cycles_recovered,
"pode_avancar": conectado and self.link_state == self.LINK_OK, # Chaves antigas preservadas, mas com semântica menos ansiosa.
"deve_conter_avanco": conectado and self.link_state == self.LINK_DEGRADED, # DEGRADED não trava avanço por si só. Só trava se for
"deve_parar_por_rede": ( # degradado restritivo ou crítico confirmado.
not conectado or self.link_state == self.LINK_CRITICAL "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_degradado": self._distancia_inicio_degradado,
"distancia_inicio_critico": self._distancia_inicio_critico, "distancia_inicio_critico": self._distancia_inicio_critico,
"max_distancia_ok": self._max_distancia_ok, "max_distancia_ok": self._max_distancia_ok,
@ -2052,6 +2229,13 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
"bandwidth_corroborated": instant[ "bandwidth_corroborated": instant[
"bandwidth_corroborated" "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), "degraded_for_s": round(degraded_for_s, 2),
"critical_for_s": round(critical_for_s, 2), "critical_for_s": round(critical_for_s, 2),
"healthy_for_s": round(healthy_for_s, 2), "healthy_for_s": round(healthy_for_s, 2),

View File

@ -116,7 +116,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
motivo = estado.get("motivo", "Estado operacional inválido para movimento") motivo = estado.get("motivo", "Estado operacional inválido para movimento")
debug["motivos"].append(motivo) debug["motivos"].append(motivo)
filtro_vel.reset(0.0) _resetar_rampa_velocidade(filtro_vel)
return _montar_comando_retorno( return _montar_comando_retorno(
velocidade_sp=0.0, velocidade_sp=0.0,
frear=True, frear=True,
@ -135,6 +135,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
reduzir_para_pulverizar = False reduzir_para_pulverizar = False
reduzir_para_fim_corredor = False reduzir_para_fim_corredor = False
reduzir_por_curva = False reduzir_por_curva = False
reduzir_por_ipb = False
if status_carro == StatusCarroMapa.Parado: if status_carro == StatusCarroMapa.Parado:
velocidade_sp = 0.0 velocidade_sp = 0.0
@ -150,7 +151,6 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
]: ]:
velocidade_sp = vel_curva velocidade_sp = vel_curva
reduzir_por_curva = True reduzir_por_curva = True
resetar_filtro = True
debug["motivos"].append("movimento em curva/manobra") debug["motivos"].append("movimento em curva/manobra")
elif status_carro in [ elif status_carro in [
@ -188,13 +188,11 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
debug["weed"] = info_ervas debug["weed"] = info_ervas
if reduzir_para_pulverizar: if reduzir_para_pulverizar:
velocidade_sp = vel_com_ervas velocidade_sp = min(velocidade_sp, vel_com_ervas)
resetar_filtro = True
debug["motivos"].append("ervas no radar: reduzindo para pulverizar") debug["motivos"].append("ervas no radar: reduzindo para pulverizar")
elif reduzir_para_fim_corredor: elif reduzir_para_fim_corredor:
velocidade_sp = vel_com_ervas velocidade_sp = min(velocidade_sp, vel_com_ervas)
resetar_filtro = True
debug["motivos"].append("fim de corredor próximo: reduzindo") debug["motivos"].append("fim de corredor próximo: reduzindo")
else: 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. # Se já decidiu parar, não faz limitador tentar reviver velocidade.
if hard_stop: if hard_stop:
filtro_vel.reset(0.0) _resetar_rampa_velocidade(filtro_vel)
return _montar_comando_retorno( return _montar_comando_retorno(
velocidade_sp=0.0, velocidade_sp=0.0,
frear=frear, 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]: if status_ipb not in [StatusModulo.OPERANTE, StatusModulo.ALERTA]:
velocidade_sp = min(velocidade_sp, vel_degradada) velocidade_sp = min(velocidade_sp, vel_degradada)
resetar_filtro = True reduzir_por_ipb = True
debug["limitadores"].append( debug["limitadores"].append(
f"IPB não operante: {status_ipb.name}, velocidade degradada" 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") debug["limitadores"].append("Visual Worker solicitou stop")
if hard_stop: if hard_stop:
filtro_vel.reset(0.0) _resetar_rampa_velocidade(filtro_vel)
return _montar_comando_retorno( return _montar_comando_retorno(
velocidade_sp=0.0, velocidade_sp=0.0,
frear=frear, frear=frear,
@ -300,7 +298,7 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
debug["limitadores"].append("IMU solicitou stop") debug["limitadores"].append("IMU solicitou stop")
if hard_stop: if hard_stop:
filtro_vel.reset(0.0) _resetar_rampa_velocidade(filtro_vel)
return _montar_comando_retorno( return _montar_comando_retorno(
velocidade_sp=0.0, velocidade_sp=0.0,
frear=frear, 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. # Reduções por segurança/curva/pulverização entram imediatamente.
# Aceleração normal é filtrada para não dar tranco. # 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: if velocidade_sp <= 0.0:
filtro_vel.reset(0.0) _resetar_rampa_velocidade(filtro_vel)
velocidade_filtrada = 0.0 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: 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( velocidade_filtrada = round(
_clamp(velocidade_filtrada, 0.0, vel_sem_ervas), _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}") mostrar_log(f"Erro ao definir o comando de movimentacao: {e}")
try: try:
filtro_vel.reset(0.0) _resetar_rampa_velocidade(filtro_vel)
except Exception: except Exception:
pass pass
@ -570,7 +579,7 @@ def _aplicar_limitador_visual(
}.get(mode, 1.00) }.get(mode, 1.00)
vel_nova = float(velocidade_atual) * fator_mode vel_nova = float(velocidade_atual) * fator_mode
resetar_filtro = fator_mode <= 0.75 resetar_filtro = False
stop = fator_mode <= 0.0 stop = fator_mode <= 0.0
debug.update({ debug.update({
@ -664,7 +673,7 @@ def _aplicar_limitador_imu(*, velocidade_atual):
return velocidade_atual, False, False, debug return velocidade_atual, False, False, debug
vel_nova = float(velocidade_atual) * velocidade_factor vel_nova = float(velocidade_atual) * velocidade_factor
resetar_filtro = velocidade_factor <= 0.75 resetar_filtro = False
return vel_nova, resetar_filtro, False, debug return vel_nova, resetar_filtro, False, debug
@ -722,6 +731,105 @@ def calcular_velocidade_relativa(
return round(_clamp(vel, vel_min, vel_max), 2) 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 # Retorno
# ============================================================ # ============================================================
@ -738,6 +846,21 @@ def _montar_comando_retorno(velocidade_sp: float, frear: bool = False, debug: di
# Helpers # 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): def _status_modulo(t_code):
modulo = _dict(ContextoGlobalRedis.get_modulo(t_code)) modulo = _dict(ContextoGlobalRedis.get_modulo(t_code))
saude = _dict(modulo.get("saude", {})) saude = _dict(modulo.get("saude", {}))

View File

@ -180,6 +180,9 @@ class CameraManager:
"campos": 0, "campos": 0,
} }
self._ultimo_frame_preview_por_tipo = {}
self._ultimo_frame_preview_ts_por_tipo = {}
def inicializar(self, mx_id): def inicializar(self, mx_id):
if self.iniciando: if self.iniciando:
return return
@ -826,13 +829,19 @@ class CameraManager:
# ============================================================ # ============================================================
def get_selected_frame(self, frame_type: TipoFrameCamera): def get_selected_frame(self, frame_type: TipoFrameCamera):
if not self._visual_disponivel_para_frame():
return None
try: 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: with self._cache_lock:
rgb = self._ultimo_rgb_frame rgb = self._copiar_frame(self._ultimo_rgb_frame)
depth = self._ultimo_depth_frame depth = self._copiar_frame(self._ultimo_depth_frame)
pred = self._ultimo_predictions pred = self._ultimo_predictions
dets = list(self._ultimo_detections or []) dets = list(self._ultimo_detections or [])
snapshot = self._ultimo_snapshot snapshot = self._ultimo_snapshot
@ -840,15 +849,15 @@ class CameraManager:
params = getattr(self.camera, "parametros", {}) if self.camera is not None else {} params = getattr(self.camera, "parametros", {}) if self.camera is not None else {}
# PosProcessamento no Visual Worker = imagem RGB original em melhor qualidade. # PosProcessamento no Visual Worker = imagem RGB original em melhor qualidade.
# Aqui não renderiza overlay, segmentação ou debug. if tipo_enum == TipoFrameCamera.PosProcessamento:
if frame_type == TipoFrameCamera.PosProcessamento: if self._frame_valido(rgb):
if rgb is None or not hasattr(rgb, "size") or rgb.size <= 0: self._set_cached_preview_frame(tipo_enum, rgb)
return None return self._copiar_frame(rgb)
return rgb.copy() return self._get_cached_preview_frame(tipo_enum)
return self.renderer.get_selected_frame( frame = self.renderer.get_selected_frame(
frame_type=frame_type, frame_type=tipo_enum,
rgb_frame=rgb, rgb_frame=rgb,
pred_ids=pred, pred_ids=pred,
depth_frame=depth, depth_frame=depth,
@ -858,10 +867,102 @@ class CameraManager:
alpha=0.50, 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: except Exception as e:
self.mostrar_log(f"[visual] erro em get_selected_frame({frame_type}): {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 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 # Loops
# ============================================================ # ============================================================
@ -1487,9 +1588,17 @@ class CameraManager:
frame = self.get_selected_frame(tipo_enum) 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 continue
frame = self._copiar_frame(frame)
nome_base = f"{frame_name}_{tipo_enum.name}" 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") caminho = os.path.join(pasta_async, f"{nome_base_async}.png")
ok = cv2.imwrite(caminho, frame_async) ok = cv2.imwrite(caminho, frame_async)
ok_final = bool(ok and os.path.exists(caminho))
t_pp1 = time.perf_counter() t_pp1 = time.perf_counter()
self.mostrar_log( self.mostrar_log(
f"[visual][SAVE_PP] salvo async | " f"[visual][SAVE_PP] salvo async | "
f"nome={nome_base_async} " f"nome={nome_base_async} "
f"ok={ok} " f"ok={ok_final} "
f"tempo_ms={(t_pp1 - t_pp0) * 1000.0:.1f}" 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. # Salva JPEG leve para não entupir disco durante operação.
caminho = os.path.join(pasta, f"{nome_base}.jpg") 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( ok = cv2.imwrite(
caminho, caminho,
frame, frame_salvar,
[int(cv2.IMWRITE_JPEG_QUALITY), int(replay_jpeg_quality)] [int(cv2.IMWRITE_JPEG_QUALITY), int(replay_jpeg_quality)]
) )
if ok: if ok and os.path.exists(caminho):
frames_salvos.append(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 return frames_salvos

View File

@ -81,6 +81,7 @@ class CameraManager:
self._lock_tensor = threading.Lock() self._lock_tensor = threading.Lock()
self._pred_lock = threading.RLock() self._pred_lock = threading.RLock()
self._pub_lock = threading.RLock() self._pub_lock = threading.RLock()
self._preview_lock = threading.RLock()
self.perf = VisualPerfMonitor(janela=180) self.perf = VisualPerfMonitor(janela=180)
@ -131,6 +132,11 @@ class CameraManager:
self._ultimo_preview_overlay = None self._ultimo_preview_overlay = None
self._ultimo_preview_debug = 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_last_ts = None
self._fps_infer_ema = 0.0 self._fps_infer_ema = 0.0
self._ultimo_infer_ms = 0.0 self._ultimo_infer_ms = 0.0
@ -612,9 +618,11 @@ class CameraManager:
if _frame_type not in self.FRAME_TYPES_PREVIEW: if _frame_type not in self.FRAME_TYPES_PREVIEW:
return None 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: 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: if (agora - self._ultimo_preview_ts) < 0.20:
cached = self._get_cached_preview_frame(_frame_type) cached = self._get_cached_preview_frame(_frame_type)
if cached is not None: if cached is not None:
@ -633,28 +641,44 @@ class CameraManager:
overlay_bgr=overlay_frame, overlay_bgr=overlay_frame,
) )
# Atualiza o timestamp da tentativa de preview.
self._ultimo_preview_ts = agora 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) return self._get_cached_preview_frame(_frame_type)
except Exception as e: except Exception as e:
self.mostrar_log(f"[weed] erro em get_selected_frame: {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): def _get_cached_preview_frame(self, frame_type):
if frame_type == TipoFrameCamera.Rgb: with self._preview_lock:
return self._ultimo_preview_rgb if frame_type == TipoFrameCamera.Rgb:
if frame_type == TipoFrameCamera.Segmentacao: frame = self._ultimo_preview_rgb
return self._ultimo_preview_seg elif frame_type == TipoFrameCamera.Segmentacao:
if frame_type == TipoFrameCamera.Overlay: frame = self._ultimo_preview_seg
return self._ultimo_preview_overlay elif frame_type == TipoFrameCamera.Overlay:
if frame_type == TipoFrameCamera.Debug: frame = self._ultimo_preview_overlay
return self._ultimo_preview_debug elif frame_type == TipoFrameCamera.Debug:
return None frame = self._ultimo_preview_debug
else:
frame = None
return self._copiar_frame(frame)
def get_debug_frame(self, mostrar=False, overlay_bgr=None): def get_debug_frame(self, mostrar=False, overlay_bgr=None):
overlay = overlay_bgr if overlay_bgr is not None else self._ultimo_preview_overlay 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}") self.mostrar_log(f"Erro ao montar debug overlay weed: {e}")
return None 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 # Loops
# ============================================================ # ============================================================
@ -1989,13 +2062,18 @@ class CameraManager:
# ==================================================== # ====================================================
# 2) FLUXO ATUAL: imagens para replay # 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 continue
if not hasattr(frame, "size") or frame.size <= 0: frame = self._copiar_frame(frame)
continue
caminho = os.path.join(pasta, f"{nome_frame}.jpg") caminho = os.path.join(pasta, f"{nome_frame}.jpg")
@ -2006,11 +2084,16 @@ class CameraManager:
else: else:
frame_salvar = frame frame_salvar = frame
cv2.imwrite( ok = cv2.imwrite(caminho, frame_salvar, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
caminho,
frame_salvar, if not ok or not os.path.exists(caminho):
[int(cv2.IMWRITE_JPEG_QUALITY), 70] self.mostrar_log(
) f"❌ Falha ao salvar frame | "
f"tipo={tipo_nome} "
f"nome={nome_frame} "
f"caminho={caminho}"
)
continue
frames_salvos.append(caminho) frames_salvos.append(caminho)

View File

@ -233,14 +233,14 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
}; };
double PotenciaBase22Psi[8] = { double PotenciaBase22Psi[8] = {
0.0, 0.0, // 0 bicos
35.0, 35.0, // 1 bico
56.4, 56.0, // 2 bicos
62.5, 63.0, // 3 bicos
65.8, 68.0, // 4 bicos
71.7, 82.0, // 5 bicos
77.5, 88.0, // 6 bicos
100.0 100.0 // 7 bicos
}; };
double PressaoReferenciaBaixaPsi = 15.0; double PressaoReferenciaBaixaPsi = 15.0;