Métricas de largura de banda módulo IPB, controle de banda no vídeo das câmeras
This commit is contained in:
parent
edb2f26550
commit
5b387de761
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue