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]
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;

View File

@ -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);
}
}

View File

@ -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,
};

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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),

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")
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", {}))

View File

@ -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

View File

@ -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)

View File

@ -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;