Métricas de largura de banda módulo IPB, controle de banda no vídeo das câmeras

This commit is contained in:
Diego Freitas 2026-03-04 23:02:13 -03:00
parent edb2f26550
commit 5b387de761
5 changed files with 417 additions and 184 deletions

View File

@ -49,7 +49,7 @@ class CameraGal:
info = self.cam.get_device_info()
#self.mostrar_log(f"Status inicial: {self.cam.get_status()}")
#self.mostrar_log(f"Device info: {info}")
self.cam.configure_fps(30)
self.cam.configure_fps(20)
self.cam.start_streaming()
self.dispositivo = T_Code.Cam
@ -70,6 +70,10 @@ class CameraGal:
self._sock_conectado = False
self._sock_ultima_tentativa_conexao = 0.0
self._sock_intervalo_reconexao = 5.0 # segundos entre tentativas
self._op_mode = 3
self._op_fps = 5
self._last_frame_sent_ts = 0.0
self._last_mode_change_ts = 0.0
if False and _ip is not None and _porta is not None:
self.mostrar_log(f"Iniciando GStreamer para {_ip}:{_porta}...")
gst_cmd = [
@ -415,34 +419,46 @@ class CameraGal:
#self.mostrar_log(f"Falha ao conectar socket de vídeo: {e}")
def enviar_frame_tcp(self, frame_bgr):
_stream_on = (ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("streaming", False)
_camera = ContextoGlobalRedis.get_camera(self.mx_id) or {}
_stream_on = _camera.get("streaming", False)
if not _stream_on:
return # se operador desligar o streaming, sai secão
# Se não está conectado, tenta conectar e sai se ainda assim não conseguir
return
# 1) Atualiza modo (não custa caro)
self._maybe_update_mode()
params = self._mode_params(self._op_mode)
self._op_fps = params["fps"]
# 2) Throttle por FPS (só tenta enviar quando "bate o relógio")
now = time.time()
min_dt = 1.0 / max(0.1, self._op_fps)
if (now - self._last_frame_sent_ts) < min_dt:
return
self._last_frame_sent_ts = now
# 4) Conexão socket (igual você já faz)
if not self._sock_conectado or self._sock is None:
self._sock_tentar_conectar()
if not self._sock_conectado or self._sock is None:
return # sem conexão, simplesmente não envia o frame
# 🔹 OPCIONAL: padronizar resolução antes de encodar
frame_bgr = resize_frame(frame_bgr, max_width=640, max_height=360)
return
# Aqui já consideramos que temos conexão, aí sim vale a pena encodar o frame
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 50] # qualidade ajustável
# 5) Resize adaptativo
frame_bgr = resize_frame(frame_bgr, max_width=params["w"], max_height=params["h"])
# 6) JPEG quality adaptativa
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(params["q"])]
ok, buf = cv2.imencode(".jpg", frame_bgr, encode_param)
if not ok:
return
data = buf.tobytes()
size = len(data)
header = struct.pack("!I", size) # 4 bytes big-endian
header = struct.pack("!I", size)
try:
self._sock.sendall(header + data)
except (BrokenPipeError, ConnectionResetError, OSError) as e:
self.mostrar_log(f"Conexão de vídeo perdida: {e}")
# marca como desconectado, próxima chamada vai tentar reconectar
self._sock_conectado = False
try:
self._sock.close()
@ -450,4 +466,47 @@ class CameraGal:
pass
self._sock = None
def _get_ipb_bw_util_pct(self) -> float:
ipb = ContextoGlobalRedis.get_modulo(T_Code.Ipb) or {} # ajuste se sua key for diferente
saude = (ipb.get("saude") or {})
detalhes = (saude.get("detalhes") or {})
return float(detalhes.get("bw_util_pct") or 0.0)
def _mode_params(self, mode: int):
# mode 0..3
table = {
3: dict(fps=5.0, q=55, w=640, h=360),
2: dict(fps=3.0, q=45, w=640, h=360),
1: dict(fps=2.0, q=40, w=480, h=270),
0: dict(fps=1.0, q=35, w=320, h=180),
}
return table.get(int(mode), table[1])
def _maybe_update_mode(self):
"""
Decide modo baseado no uso de banda. Histerese com cooldown pra não oscilar.
"""
util = self._get_ipb_bw_util_pct() # 0..150 (pode passar 100)
now = time.time()
# cooldown mínimo entre trocas
if (now - self._last_mode_change_ts) < 1.5:
return
# thresholds (ajuste fino depois)
# sobe só se estiver bem folgado
if util < 55 and self._op_mode < 3:
self._op_mode += 1
self._last_mode_change_ts = now
# desce se está pesado
elif util > 85 and self._op_mode > 0:
self._op_mode -= 1
self._last_mode_change_ts = now
# emergência: muito alto, cai mais
if util > 90 and self._op_mode > 0:
self._op_mode = max(0, self._op_mode - 1)
self._last_mode_change_ts = now

View File

@ -92,6 +92,10 @@ class CameraOak:
self._sock_conectado = False
self._sock_ultima_tentativa_conexao = 0.0
self._sock_intervalo_reconexao = 5.0 # segundos entre tentativas
self._op_mode = 3
self._op_fps = 5
self._last_frame_sent_ts = 0.0
self._last_mode_change_ts = 0.0
if False and _ip is not None and _porta is not None:
self.mostrar_log(f"Iniciando GStreamer para {_ip}:{_porta}...")
gst_cmd = [
@ -743,34 +747,45 @@ class CameraOak:
#self.mostrar_log(f"Falha ao conectar socket de vídeo: {e}")
def enviar_frame_tcp(self, frame_bgr):
_stream_on = (ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("streaming", False)
_camera = ContextoGlobalRedis.get_camera(self.mx_id) or {}
_stream_on = _camera.get("streaming", False)
if not _stream_on:
return # se operador desligar o streaming, sai secão
# Se não está conectado, tenta conectar e sai se ainda assim não conseguir
return
# 1) Atualiza modo (não custa caro)
self._maybe_update_mode()
params = self._mode_params(self._op_mode)
self._op_fps = params["fps"]
# 2) Throttle por FPS (só tenta enviar quando "bate o relógio")
now = time.time()
min_dt = 1.0 / max(0.1, self._op_fps)
if (now - self._last_frame_sent_ts) < min_dt:
return
self._last_frame_sent_ts = now
# 4) Conexão socket (igual você já faz)
if not self._sock_conectado or self._sock is None:
self._sock_tentar_conectar()
if not self._sock_conectado or self._sock is None:
return # sem conexão, simplesmente não envia o frame
# 🔹 OPCIONAL: padronizar resolução antes de encodar
frame_bgr = resize_frame(frame_bgr, max_width=640, max_height=360)
return
# Aqui já consideramos que temos conexão, aí sim vale a pena encodar o frame
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 50] # qualidade ajustável
# 5) Resize adaptativo
frame_bgr = resize_frame(frame_bgr, max_width=params["w"], max_height=params["h"])
# 6) JPEG quality adaptativa
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(params["q"])]
ok, buf = cv2.imencode(".jpg", frame_bgr, encode_param)
if not ok:
return
data = buf.tobytes()
size = len(data)
header = struct.pack("!I", size) # 4 bytes big-endian
header = struct.pack("!I", size)
try:
self._sock.sendall(header + data)
except (BrokenPipeError, ConnectionResetError, OSError) as e:
self.mostrar_log(f"Conexão de vídeo perdida: {e}")
# marca como desconectado, próxima chamada vai tentar reconectar
self._sock_conectado = False
try:
self._sock.close()
@ -778,5 +793,45 @@ class CameraOak:
pass
self._sock = None
def _get_ipb_bw_util_pct(self) -> float:
ipb = ContextoGlobalRedis.get_modulo(T_Code.Ipb) or {} # ajuste se sua key for diferente
saude = (ipb.get("saude") or {})
detalhes = (saude.get("detalhes") or {})
return float(detalhes.get("bw_util_pct") or 0.0)
def _mode_params(self, mode: int):
# mode 0..3
table = {
3: dict(fps=5.0, q=55, w=640, h=360),
2: dict(fps=3.0, q=45, w=640, h=360),
1: dict(fps=2.0, q=40, w=480, h=270),
0: dict(fps=1.0, q=35, w=320, h=180),
}
return table.get(int(mode), table[1])
def _maybe_update_mode(self):
"""
Decide modo baseado no uso de banda. Histerese com cooldown pra não oscilar.
"""
util = self._get_ipb_bw_util_pct() # 0..150 (pode passar 100)
now = time.time()
# cooldown mínimo entre trocas
if (now - self._last_mode_change_ts) < 1.5:
return
# thresholds (ajuste fino depois)
# sobe só se estiver bem folgado
if util < 55 and self._op_mode < 3:
self._op_mode += 1
self._last_mode_change_ts = now
# desce se está pesado
elif util > 85 and self._op_mode > 0:
self._op_mode -= 1
self._last_mode_change_ts = now
# emergência: muito alto, cai mais
if util > 90 and self._op_mode > 0:
self._op_mode = max(0, self._op_mode - 1)
self._last_mode_change_ts = now

View File

@ -13,7 +13,7 @@ from shared.enums import StatusModulo, T_Code
from health_worker.modulos.base import ModuloDiagnosticoBase
class ModuloIPBribge(ModuloDiagnosticoBase):
def __init__(self, window_size: int = 5):
def __init__(self, window_fast: int = 6, window_slow: int = 20):
self.t_code = T_Code.Ipb
self.nome = "IP_Brigde"
self.timeout = 5
@ -22,21 +22,30 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
self._running = False
self.nic_name = self.descobrir_nic_para_base()
self.window_size = window_size
self.window_fast = window_fast
self.window_slow = window_slow
self.rtts = deque(maxlen=window_size) # ping válidos (ms)
self.timeouts = deque(maxlen=window_size) # True/False por tentativa
self.loses = deque(maxlen=window_size) # True/False por tentativa
# FAST (reação)
self.rtts_f = deque(maxlen=window_fast)
self.timeouts_f = deque(maxlen=window_fast)
self.loses_f = deque(maxlen=window_fast)
self.nic_error_rates_f = deque(maxlen=window_fast)
self.bw_rx_mbps_f = deque(maxlen=window_fast)
self.bw_tx_mbps_f = deque(maxlen=window_fast)
# SLOW (estabilidade)
self.rtts_s = deque(maxlen=window_slow)
self.timeouts_s = deque(maxlen=window_slow)
self.loses_s = deque(maxlen=window_slow)
self.nic_error_rates_s = deque(maxlen=window_slow)
self.bw_rx_mbps_s = deque(maxlen=window_slow)
self.bw_tx_mbps_s = deque(maxlen=window_slow)
self.last_nic_counters = None
self.nic_error_rates = deque(maxlen=window_size)
self.last_bw_counters = None
self.last_bw_ts = None
self.bw_rx_mbps = deque(maxlen=window_size)
self.bw_tx_mbps = deque(maxlen=window_size)
self.last_heartbeat_ts = time.time() # atualize isso de fora
self.last_heartbeat_ts = time.time()
self.rover_id = None
self.sub = False
@ -66,6 +75,10 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
self._ping_count = 2 # recomendado: 1 (janela já suaviza)
self._ping_min_interval = 0.6 # não precisa pingar a cada 100ms
self.bw_max_mbps = 2.0 # capacidade aproximada do link (ajuste por teste)
self.bw_safe_mbps = 1.2 # alvo saudável (até aqui não penaliza)
self.bw_hard_mbps = 1.8 # acima disso é zona vermelha
def _start_ping_async_if_needed(self):
@ -262,12 +275,14 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
#print("[HEARTBEAT] recebido")
def _reset_janelas(self):
self.rtts.clear()
self.timeouts.clear()
self.loses.clear()
self.nic_error_rates.clear()
self.bw_rx_mbps.clear()
self.bw_tx_mbps.clear()
for d in (
self.rtts_f, self.timeouts_f, self.loses_f,
self.nic_error_rates_f, self.bw_rx_mbps_f, self.bw_tx_mbps_f,
self.rtts_s, self.timeouts_s, self.loses_s,
self.nic_error_rates_s, self.bw_rx_mbps_s, self.bw_tx_mbps_s,
):
d.clear()
self.last_nic_counters = None
self.last_bw_counters = None
self.last_bw_ts = None
@ -329,8 +344,10 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
rx_mbps = (delta_rx * 8.0) / (dt * 1_000_000.0)
tx_mbps = (delta_tx * 8.0) / (dt * 1_000_000.0)
self.bw_rx_mbps.append(rx_mbps)
self.bw_tx_mbps.append(tx_mbps)
self.bw_rx_mbps_f.append(rx_mbps)
self.bw_tx_mbps_f.append(tx_mbps)
self.bw_rx_mbps_s.append(rx_mbps)
self.bw_tx_mbps_s.append(tx_mbps)
self.last_bw_counters = counters
self.last_bw_ts = now
@ -438,12 +455,12 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
if delta_pkts > 0:
# erros por mil pacotes
rate = (delta_err / delta_pkts) * 1000.0
self.nic_error_rates.append(rate)
self.nic_error_rates_f.append(rate)
self.nic_error_rates_s.append(rate)
self.last_nic_counters = counters
def _metric_scores(self):
# valores brutos (default)
def _metric_scores_from(self, rtts, timeouts, loses, nic_error_rates):
avg_rtt = None
loss_pct = 100.0
jitter = 0.0
@ -451,99 +468,106 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
atraso = time.time() - self.last_heartbeat_ts
# LATÊNCIA
if self.rtts:
avg_rtt = statistics.mean(self.rtts)
if avg_rtt <= 30:
latency_score = 100
elif avg_rtt <= 50:
latency_score = 80
elif avg_rtt <= 100:
latency_score = 50
elif avg_rtt <= 200:
latency_score = 20
else:
latency_score = 0
if rtts:
avg_rtt = statistics.mean(rtts)
if avg_rtt <= 30: latency_score = 100
elif avg_rtt <= 50: latency_score = 80
elif avg_rtt <= 100: latency_score = 50
elif avg_rtt <= 200: latency_score = 20
else: latency_score = 0
else:
latency_score = 0
# TIMEOUT
if self.timeouts:
timeout_pct = (sum(self.timeouts) / len(self.timeouts)) * 100.0
if timeouts:
timeout_pct = (sum(timeouts) / len(timeouts)) * 100.0
else:
timeout_pct = 100.0
if timeout_pct <= 5:
timeout_score = 100
elif timeout_pct <= 10:
timeout_score = 80
elif timeout_pct <= 15:
timeout_score = 50
elif timeout_pct <= 30:
timeout_score = 20
else:
timeout_score = 0
if timeout_pct <= 5: timeout_score = 100
elif timeout_pct <= 10: timeout_score = 80
elif timeout_pct <= 15: timeout_score = 50
elif timeout_pct <= 30: timeout_score = 20
else: timeout_score = 0
# LOSS
if self.loses:
loss_pct = (sum(self.loses) / len(self.loses))
if loses:
loss_pct = (sum(loses) / len(loses))
else:
loss_pct = 100.0
if loss_pct <= 2:
loss_score = 100
elif loss_pct <= 5:
loss_score = 80
elif loss_pct <= 8:
loss_score = 50
elif loss_pct <= 15:
loss_score = 20
else:
loss_score = 0
if loss_pct <= 2: loss_score = 100
elif loss_pct <= 5: loss_score = 80
elif loss_pct <= 8: loss_score = 50
elif loss_pct <= 15: loss_score = 20
else: loss_score = 0
# JITTER
if len(self.rtts) > 2:
jitter = statistics.pstdev(self.rtts)
if jitter <= 10:
jitter_score = 100
elif jitter <= 20:
jitter_score = 70
elif jitter <= 40:
jitter_score = 30
else:
jitter_score = 0
if len(rtts) > 2:
jitter = statistics.pstdev(rtts)
if jitter <= 10: jitter_score = 100
elif jitter <= 20: jitter_score = 70
elif jitter <= 40: jitter_score = 30
else: jitter_score = 0
else:
jitter = 0.0
# Sem dados suficientes, melhor considerar "desconhecido" => score neutro/baixo
jitter_score = 1
# NIC ERRORS
if self.nic_error_rates:
err_rate = statistics.mean(self.nic_error_rates)
if nic_error_rates:
err_rate = statistics.mean(nic_error_rates)
else:
err_rate = 0.0
if err_rate <= 0.1:
nic_score = 100
elif err_rate <= 1:
nic_score = 70
elif err_rate <= 5:
nic_score = 40
else:
nic_score = 0
if err_rate <= 0.1: nic_score = 100
elif err_rate <= 1: nic_score = 70
elif err_rate <= 5: nic_score = 40
else: nic_score = 0
# HEARTBEAT (score usando "atraso")
t0, t1 = 1.0, 3.0 # 1s perfeito, 3s zerou
x = (atraso - t0) / (t1 - t0) # 0..1
# HEARTBEAT (igual)
t0, t1 = 1.0, 3.0
x = (atraso - t0) / (t1 - t0)
x = max(0.0, min(1.0, x))
gamma = 1.6 # >1 deixa cair mais rápido perto do final
gamma = 1.6
hb_score = int(round(100 * (1.0 - (x ** gamma))))
# retorna scores + brutos
return (
latency_score, timeout_score, loss_score, jitter_score, nic_score, hb_score,
avg_rtt, timeout_pct, loss_pct, jitter, err_rate, atraso
)
def _bandwidth_score(self, bw_total_mbps: float):
"""
Retorna (bw_score 0..100, bw_util_pct 0..100, bw_state str)
Curva pensada p/ rádio: até SAFE = ok, depois cai, e perto do HARD cai forte.
"""
bw = max(0.0, float(bw_total_mbps or 0.0))
bw_max = max(0.1, float(self.bw_max_mbps))
bw_safe = max(0.1, float(self.bw_safe_mbps))
bw_hard = max(bw_safe, float(self.bw_hard_mbps))
util = min(1.5, bw / bw_max) # pode passar de 100% (indicador)
util_pct = util * 100.0
if bw <= bw_safe:
return 100.0, util_pct, "OK"
if bw >= bw_hard:
# acima do hard: score vai rapidamente pra 0
# 0 no hard*1.1, por exemplo
span = max(0.05, bw_hard * 0.10)
x = min(1.0, (bw - bw_hard) / span)
score = max(0.0, 20.0 * (1.0 - x)) # 20 → 0
return score, util_pct, "CRITICO"
# entre safe e hard: cai de 100 → 20
x = (bw - bw_safe) / max(0.05, (bw_hard - bw_safe)) # 0..1
score = 100.0 - (80.0 * x) # 100 → 20
state = "ALTO" if x >= 0.5 else "MEDIO"
return max(0.0, min(100.0, score)), util_pct, state
def atualizar_saude_interno(self):
#print("atualizando saude IPB...")
try:
@ -554,7 +578,8 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
motivos = []
condicoes = []
saude_individual = []
conectado = self.get_base_ip() is not None and self.nic_name is not None and self._mqtt_conectado
#conectado = self.get_base_ip() is not None and self.nic_name is not None and self._mqtt_conectado
conectado = (self.get_base_ip() is not None) and self._mqtt_conectado
# 1) consome resultado pronto (não bloqueia)
self._consume_ping_result_if_ready()
@ -563,11 +588,16 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
# 3) usa snapshot pra alimentar suas janelas rtts/loses/timeouts
ok, rtt, loss, ping_ts = self._get_ping_snapshot()
#print(f"ok: {ok}, rtt: {rtt}, loss: {loss}, ping_ts: {ping_ts}")
self.timeouts.append(loss == 100)
self.timeouts_f.append(loss == 100)
self.timeouts_s.append(loss == 100)
if ok and rtt is not None:
self.rtts.append(rtt)
self.rtts_f.append(rtt)
self.rtts_s.append(rtt)
if loss is not None:
self.loses.append(loss)
self.loses_f.append(loss)
self.loses_s.append(loss)
#(rtt, loss) = self._ping_once_old(pings=3, timeout=200)
#print(f"rtt: {rtt}, loss: {loss}")
@ -580,29 +610,93 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
self._update_nic_errors()
self._update_nic_bandwidth()
(ls, tmout, los, js, ns, hbs, avg_rtt, tmout_pct, loss_pct, jitter, err_rate, atraso) = self._metric_scores()
fast = self._metric_scores_from(self.rtts_f, self.timeouts_f, self.loses_f, self.nic_error_rates_f)
slow = self._metric_scores_from(self.rtts_s, self.timeouts_s, self.loses_s, self.nic_error_rates_s)
(ls_f, tmout_f, los_f, js_f, ns_f, hbs_f, avg_rtt_f, tmout_pct_f, loss_pct_f, jitter_f, err_rate_f, atraso_f) = fast
(ls_s, tmout_s, los_s, js_s, ns_s, hbs_s, avg_rtt_s, tmout_pct_s, loss_pct_s, jitter_s, err_rate_s, atraso_s) = slow
rx_f = statistics.mean(self.bw_rx_mbps_f) if self.bw_rx_mbps_f else 0.0
tx_f = statistics.mean(self.bw_tx_mbps_f) if self.bw_tx_mbps_f else 0.0
bw_total_f = rx_f + tx_f
bw_score_f, bw_util_pct_f, bw_state_f = self._bandwidth_score(bw_total_f)
rx_s = statistics.mean(self.bw_rx_mbps_s) if self.bw_rx_mbps_s else 0.0
tx_s = statistics.mean(self.bw_tx_mbps_s) if self.bw_tx_mbps_s else 0.0
bw_total_s = rx_s + tx_s
bw_score_s, bw_util_pct_s, bw_state_s = self._bandwidth_score(bw_total_s)
health = (
0.25 * ls +
0.10 * tmout +
0.30 * los +
0.15 * js +
0.10 * ns +
0.10 * hbs
health_f = (
0.20 * ls_f +
0.10 * tmout_f +
0.25 * los_f +
0.10 * js_f +
0.10 * ns_f +
0.10 * hbs_f +
0.15 * bw_score_f
)
health_s = (
0.20 * ls_s +
0.10 * tmout_s +
0.25 * los_s +
0.10 * js_s +
0.10 * ns_s +
0.10 * hbs_s +
0.15 * bw_score_s
)
health = 0.70 * health_s + 0.30 * health_f
# Se o FAST gritar "caos", derruba a saúde na hora (não espera a slow)
if loss_pct_f >= 8 or tmout_pct_f >= 10:
health = min(health, 45)
# Se saturou banda no FAST, corta também (comandos atrasam)
if bw_util_pct_f >= 95:
health = min(health, 55)
# Se heartbeat atrasou muito, isso é sério
if atraso_f >= 3.0:
health = min(health, 40)
saude = round(health, 1)
# -------- SAÚDE INDIVIDUAL / CONDIÇÕES / MOTIVOS --------
def _mix(a_s, a_f, w_s=0.70, w_f=0.30):
return (w_s * a_s) + (w_f * a_f)
ls = _mix(ls_s, ls_f)
tmout = _mix(tmout_s, tmout_f)
los = _mix(los_s, los_f)
js = _mix(js_s, js_f)
ns = _mix(ns_s, ns_f)
hbs = _mix(hbs_s, hbs_f)
bw_score = _mix(bw_score_s, bw_score_f)
# Para valores brutos, eu sugiro mostrar SLOW como "tendência" e FAST como "agora".
avg_rtt = avg_rtt_s
tmout_pct = tmout_pct_s
loss_pct = loss_pct_s
jitter = jitter_s
err_rate = err_rate_s
atraso = atraso_s
rx_mbps = rx_s
tx_mbps = tx_s
bw_total = bw_total_s
bw_util_pct = bw_util_pct_s
bw_state = bw_state_s
# 1) Latência
cond_lat = []
if ls < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - ls))
if ls_f < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - ls_f))
c = {
"label": "Latência ICMP",
"valor": avg_rtt if avg_rtt is not None else -1,
"valor": avg_rtt_f if avg_rtt_f is not None else -1,
"severidade": severidade,
"descricao": "Latência alta na comunicação com a base",
"descricao": f"Latência alta (rápida). Tendência: {avg_rtt_s:.0f} ms" if avg_rtt_s is not None else "Latência alta (rápida).",
"acoes": [
"Reduzir taxa de envio de telemetria",
"Verificar alinhamento das antenas 900 MHz",
@ -611,32 +705,31 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
}
condicoes.append(c)
cond_lat.append(c)
motivos.append(
f"Latência ICMP elevada "
f"({avg_rtt:.0f} ms, score {ls:.0f})."
if avg_rtt is not None else
f"Latência ICMP comprometida (score {ls:.0f})."
)
if avg_rtt_f is not None:
motivos.append(f"Latência ICMP alta (fast {avg_rtt_f:.0f} ms, score {ls_f:.0f}; slow {avg_rtt_s:.0f} ms, score {ls_s:.0f}).")
else:
motivos.append(f"Latência ICMP comprometida (fast score {ls_f:.0f}; slow score {ls_s:.0f}).")
saude_individual.append({
"id": "ip_latency",
"label": "Latência ICMP",
"status": self._status_por_score(ls).value,
"saude": round(ls, 1),
"motivos": [m for m in motivos if "Latência" in m],
"status": self._status_por_score(ls).value, # <- MIX
"saude": round(ls, 1), # <- MIX
"motivos": [m for m in motivos if "Latência ICMP" in m],
"condicoes_operacionais": cond_lat,
"em_uso": True,
})
# 2) Timeouts
cond_timeout = []
if tmout < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - tmout))
if tmout_f < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - tmout_f))
c = {
"label": "Timeout",
"valor": round(tmout_pct, 2),
"valor": round(tmout_pct_f, 2),
"severidade": severidade,
"descricao": "Timeout na comunicação ICMP",
"descricao": f"Timeout ICMP alto (rápido). Tendência: {tmout_pct_s:.1f}%",
"acoes": [
"Checar conectores e cabo da ponte 900 MHz",
"Verificar nível de ruído / interferência no enlace",
@ -645,29 +738,27 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
}
condicoes.append(c)
cond_timeout.append(c)
motivos.append(
f"Timeout elevado ({tmout_pct:.1f}%, score {tmout:.0f})."
)
motivos.append(f"Timeout alto (fast {tmout_pct_f:.1f}%, score {tmout_f:.0f}; slow {tmout_pct_s:.1f}%, score {tmout_s:.0f}).")
saude_individual.append({
"id": "ip_timeout",
"label": "Timeout",
"status": self._status_por_score(tmout).value,
"saude": round(tmout, 1),
"motivos": [m for m in motivos if "Timeout" in m],
"motivos": [m for m in motivos if "Timeout alto" in m or "Timeout ICMP" in m],
"condicoes_operacionais": cond_timeout,
"em_uso": True,
})
# 2) Perda de pacotes
cond_loss = []
if los < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - los))
if los_f < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - los_f))
c = {
"label": "Perda de Pacotes",
"valor": round(loss_pct, 2),
"valor": round(loss_pct_f, 2),
"severidade": severidade,
"descricao": "Perda de pacotes na comunicação ICMP",
"descricao": f"Perda de pacotes alta (rápida). Tendência: {loss_pct_s:.1f}%",
"acoes": [
"Checar conectores e cabo da ponte 900 MHz",
"Verificar nível de ruído / interferência no enlace",
@ -676,29 +767,27 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
}
condicoes.append(c)
cond_loss.append(c)
motivos.append(
f"Perda de pacotes elevada ({loss_pct:.1f}%, score {los:.0f})."
)
motivos.append(f"Perda de pacotes alta (fast {loss_pct_f:.1f}%, score {los_f:.0f}; slow {loss_pct_s:.1f}%, score {los_s:.0f}).")
saude_individual.append({
"id": "ip_loss",
"label": "Perda de pacotes",
"status": self._status_por_score(los).value,
"saude": round(los, 1),
"motivos": [m for m in motivos if "Perda de pacotes" in m],
"motivos": [m for m in motivos if "Perda de pacotes alta" in m],
"condicoes_operacionais": cond_loss,
"em_uso": True,
})
# 3) Jitter
cond_jit = []
if js < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - js))
if js_f < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - js_f))
c = {
"label": "Jitter (variação de latência)",
"valor": round(jitter, 2),
"valor": round(jitter_f, 2),
"severidade": severidade,
"descricao": "Variação de latência acima do ideal",
"descricao": f"Jitter alto (rápido). Tendência: {jitter_s:.1f} ms",
"acoes": [
"Evitar tráfego pesado na mesma rede da ponte",
"Reduzir taxa de envio de mensagens de controle",
@ -707,60 +796,56 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
}
condicoes.append(c)
cond_jit.append(c)
motivos.append(
f"Jitter elevado ({jitter:.1f} ms, score {js:.0f})."
)
motivos.append(f"Jitter alto (fast {jitter_f:.1f} ms, score {js_f:.0f}; slow {jitter_s:.1f} ms, score {js_s:.0f}).")
saude_individual.append({
"id": "ip_jitter",
"label": "Jitter da conexão",
"status": self._status_por_score(js).value,
"saude": round(js, 1),
"motivos": [m for m in motivos if "Jitter" in m],
"motivos": [m for m in motivos if "Jitter alto" in m],
"condicoes_operacionais": cond_jit,
"em_uso": True,
})
# 4) Erros na NIC
cond_nic = []
if ns < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - ns))
if ns_f < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - ns_f))
c = {
"label": "Erros na interface de rede",
"valor": round(err_rate, 3),
"valor": round(err_rate_f, 3),
"severidade": severidade,
"descricao": "Erros na placa de rede ligada à ponte 900 MHz",
"descricao": f"Erros na NIC acima do normal (rápido). Tendência: {err_rate_s:.3f}",
"acoes": [
"Verificar cabo de rede e conectores",
"Checar se há colisões ou problemas físicos no link",
"Checar colisões ou problemas físicos no link",
"Substituir cabo ou porta de switch se necessário"
]
}
condicoes.append(c)
cond_nic.append(c)
motivos.append(
f"Erros na NIC acima do normal ({err_rate:.3f} erros/mil pacotes, score {ns:.0f})."
)
motivos.append(f"Erros NIC altos (fast {err_rate_f:.3f}, score {ns_f:.0f}; slow {err_rate_s:.3f}, score {ns_s:.0f}).")
saude_individual.append({
"id": "ip_nic_errors",
"label": "Erros da interface de rede",
"status": self._status_por_score(ns).value,
"saude": round(ns, 1),
"motivos": [m for m in motivos if "NIC" in m or "interface de rede" in m],
"motivos": [m for m in motivos if "Erros NIC altos" in m],
"condicoes_operacionais": cond_nic,
"em_uso": True,
})
# 5) Heartbeat
cond_hb = []
if hbs < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - hbs))
if hbs_f < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - hbs_f))
c = {
"label": "Atraso de heartbeat",
"valor": round(atraso, 2),
"valor": round(atraso_f, 2),
"severidade": severidade,
"descricao": "Atraso na recepção de heartbeat do rover",
"descricao": f"Heartbeat atrasado (rápido). Tendência: {atraso_s:.1f}s",
"acoes": [
"Verificar estado do serviço de telemetria no rover",
"Checar fila de mensagens MQTT/Redis",
@ -769,23 +854,49 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
}
condicoes.append(c)
cond_hb.append(c)
motivos.append(
f"Heartbeat atrasado ({atraso:.1f} s sem atualização, score {hbs:.0f})."
)
motivos.append(f"Heartbeat atrasado (fast {atraso_f:.1f}s, score {hbs_f:.0f}; slow {atraso_s:.1f}s, score {hbs_s:.0f}).")
saude_individual.append({
"id": "ip_heartbeat",
"label": "Heartbeat do rover",
"status": self._status_por_score(hbs).value,
"saude": round(hbs, 1),
"motivos": [m for m in motivos if "Heartbeat" in m],
"motivos": [m for m in motivos if "Heartbeat atrasado" in m],
"condicoes_operacionais": cond_hb,
"em_uso": True,
})
rx_mbps = statistics.mean(self.bw_rx_mbps) if self.bw_rx_mbps else 0.0
tx_mbps = statistics.mean(self.bw_tx_mbps) if self.bw_tx_mbps else 0.0
bw_total = rx_mbps + tx_mbps
# 6) Banda / Saturação
cond_bw = []
if bw_score_f < SAUDE_MIN_ALERTA:
severidade = int(max(0, 100 - bw_score_f))
c = {
"label": "Uso de banda",
"valor": round(bw_util_pct_f, 1),
"severidade": severidade,
"descricao": f"Uso de banda alto (rápido: {bw_state_f}). Tendência: {bw_util_pct_s:.0f}% ({bw_state_s})",
"acoes": [
"Reduzir FPS/qualidade/resolução do vídeo",
"Evitar envio de logs em rajada",
"Priorizar tráfego de controle"
]
}
condicoes.append(c)
cond_bw.append(c)
motivos.append(
f"Banda alta (fast {bw_total_f:.2f} Mbps, {bw_util_pct_f:.0f}%, score {bw_score_f:.0f}; "
f"slow {bw_total_s:.2f} Mbps, {bw_util_pct_s:.0f}%, score {bw_score_s:.0f})."
)
saude_individual.append({
"id": "ip_bandwidth",
"label": "Uso de banda",
"status": self._status_por_score(bw_score).value, # MIX
"saude": round(bw_score, 1), # MIX
"motivos": [m for m in motivos if "Banda alta" in m],
"condicoes_operacionais": cond_bw,
"em_uso": True,
})
# -------- STATUS GERAL DO MÓDULO --------
status = StatusModulo.OPERANTE
@ -818,7 +929,13 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
"atraso_hb": atraso,
"rx_mbps": rx_mbps,
"tx_mbps": tx_mbps,
"bw_total_mbps": bw_total
"bw_total_mbps": bw_total,
"bw_score": bw_score,
"bw_util_pct": bw_util_pct,
"bw_state": bw_state,
"bw_max_mbps": self.bw_max_mbps,
"bw_safe_mbps": self.bw_safe_mbps,
"bw_hard_mbps": self.bw_hard_mbps,
}
}

View File

@ -115,7 +115,7 @@ class CameraManager:
self._analisando_deteccao = False
self._iniciar_loop_analise_continua(15.0)
self._iniciar_loop_frame_stream(self.camera.gst_FPS)
self._iniciar_loop_frame_stream(self.camera._op_fps)
self.iniciando = False
self.atualizar_saude_camera()
@ -318,6 +318,7 @@ class CameraManager:
self.mostrar_log(f"Erro no loop de stream: {e}")
finally:
latencia = time.time() - t0
freq = self.camera._op_fps
time.sleep(max(0, (1.0 / freq) - latencia))
threading.Thread(target=loop, daemon=True).start()

View File

@ -79,7 +79,7 @@ class CameraManager:
self.weed_detector = WeedDetector(color_map=colormap_rgb, classes=classes)
self._iniciar_loop_analise_continua(20.0)
self._iniciar_loop_frame_stream(self.camera.gst_FPS)
self._iniciar_loop_frame_stream(self.camera._op_fps)
self.iniciando = False
self.atualizar_saude_camera()
@ -215,6 +215,7 @@ class CameraManager:
self.mostrar_log(f"Erro no loop de stream: {e}")
finally:
latencia = time.time() - t0
freq = self.camera._op_fps
time.sleep(max(0, (1.0 / freq) - latencia))
threading.Thread(target=loop, daemon=True).start()