correcoes e robustez no fluxo de recuperacao da camera traseira de ervas oak-ffc-3p multiespectral
This commit is contained in:
parent
2936df66fa
commit
7d63265ad1
|
|
@ -81,6 +81,10 @@ class CameraMultispectral:
|
|||
self.ultimo_meta = None
|
||||
self.ultimo_decoded = None
|
||||
|
||||
self._vida_lock = threading.RLock()
|
||||
self._parando = False
|
||||
self.client = None
|
||||
|
||||
self._ultimo_resultado_tensor = {
|
||||
"erro": "sem tensor em cache",
|
||||
"duracao": 0.0,
|
||||
|
|
@ -99,6 +103,12 @@ class CameraMultispectral:
|
|||
"frame_valido": False,
|
||||
}
|
||||
|
||||
self._ultimo_pipeline_ia = {
|
||||
"ok": False,
|
||||
"timestamp": 0.0,
|
||||
"motivos": ["pipeline IA ainda não validado"],
|
||||
}
|
||||
|
||||
self.parametros = {}
|
||||
|
||||
if module_calibration_json is None:
|
||||
|
|
@ -203,9 +213,18 @@ class CameraMultispectral:
|
|||
)
|
||||
|
||||
except Exception as e:
|
||||
client_parcial = getattr(self, "client", None)
|
||||
|
||||
try:
|
||||
if client_parcial is not None:
|
||||
client_parcial.stop()
|
||||
except Exception as stop_error:
|
||||
self.mostrar_log(f"[CameraMultispectral] Erro ao limpar início parcial: {stop_error}")
|
||||
|
||||
self.client = None
|
||||
self.iniciado = False
|
||||
self.rodando = False
|
||||
|
||||
self.mostrar_log(f"[CameraMultispectral] Erro ao iniciar módulo: {e}")
|
||||
|
||||
ContextoGlobalRedis.atualizar_ctx_dict(
|
||||
|
|
@ -283,54 +302,79 @@ class CameraMultispectral:
|
|||
# ============================================================
|
||||
|
||||
def parar(self):
|
||||
self.rodando = False
|
||||
self.iniciado = False
|
||||
with self._vida_lock:
|
||||
if self._parando:
|
||||
return
|
||||
|
||||
self._parando = True
|
||||
|
||||
client = self.client
|
||||
imu = self.imu
|
||||
stream = self.stream
|
||||
|
||||
# Desacopla as referências primeiro.
|
||||
self.client = None
|
||||
self.imu = None
|
||||
self.q_imu = None
|
||||
self.stream = None
|
||||
|
||||
self.tem_imu = False
|
||||
self.rodando = False
|
||||
self.iniciado = False
|
||||
|
||||
try:
|
||||
if self.imu is not None:
|
||||
self.imu.parar()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.imu = None
|
||||
self.q_imu = None
|
||||
self.tem_imu = False
|
||||
|
||||
try:
|
||||
if self.client is not None:
|
||||
self.client.stop()
|
||||
except Exception as e:
|
||||
try:
|
||||
self.mostrar_log(f"[CameraMultispectral] Erro ao parar client: {e}")
|
||||
if imu is not None:
|
||||
imu.parar()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.client = None
|
||||
try:
|
||||
if client is not None:
|
||||
client.stop()
|
||||
except Exception as e:
|
||||
self.mostrar_log(
|
||||
f"[CameraMultispectral] Erro ao parar client: {e}"
|
||||
)
|
||||
|
||||
try:
|
||||
if self.stream is not None and hasattr(self.stream, "parar"):
|
||||
self.stream.parar()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if stream is not None and hasattr(stream, "parar"):
|
||||
stream.parar()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.stream = None
|
||||
with self._lock:
|
||||
self.ultimo_tensor_multispec = None
|
||||
self.ultimo_frame_rgb = None
|
||||
self.ultimo_raw_multi = None
|
||||
self.ultimo_meta = None
|
||||
self.ultimo_decoded = None
|
||||
|
||||
with self._lock:
|
||||
self.ultimo_tensor_multispec = None
|
||||
self.ultimo_frame_rgb = None
|
||||
self.ultimo_raw_multi = None
|
||||
self.ultimo_meta = None
|
||||
self.ultimo_decoded = None
|
||||
self.timestamp_ultimo_tensor = None
|
||||
self.timestamp_ultimo_frame_rgb = None
|
||||
self.timestamp_ultimo_raw_multi = None
|
||||
|
||||
self.timestamp_ultimo_tensor = None
|
||||
self.timestamp_ultimo_frame_rgb = None
|
||||
self.timestamp_ultimo_raw_multi = None
|
||||
self._ultimo_resultado_tensor = {
|
||||
"erro": "pipeline parado",
|
||||
"duracao": 0.0,
|
||||
"frame_valido": False,
|
||||
}
|
||||
|
||||
self._ultimo_resultado_tensor = {
|
||||
"erro": "pipeline parado",
|
||||
"duracao": 0.0,
|
||||
"frame_valido": False,
|
||||
}
|
||||
self._ultimo_resultado_rgb = {
|
||||
"erro": "pipeline parado",
|
||||
"duracao": 0.0,
|
||||
"frame_valido": False,
|
||||
}
|
||||
|
||||
self._ultimo_resultado_raw = {
|
||||
"erro": "pipeline parado",
|
||||
"duracao": 0.0,
|
||||
"frame_valido": False,
|
||||
}
|
||||
|
||||
finally:
|
||||
with self._vida_lock:
|
||||
self._parando = False
|
||||
|
||||
def _is_erro_fatal_depthai(self, erro):
|
||||
if erro is None:
|
||||
|
|
@ -525,6 +569,9 @@ class CameraMultispectral:
|
|||
ts = time.time()
|
||||
dur = time.perf_counter() - t_total0
|
||||
|
||||
meta = dict(meta or {})
|
||||
capture_perf = dict(meta.get("capture_perf", {}) or {})
|
||||
|
||||
resultado = {
|
||||
"erro": None,
|
||||
"duracao": dur,
|
||||
|
|
@ -532,8 +579,15 @@ class CameraMultispectral:
|
|||
"shape": list(tensor.shape),
|
||||
"dtype": str(tensor.dtype),
|
||||
"channels": ["R", "G", "B", "RE", "NIR"],
|
||||
"sync_ok": bool(meta.get("sync_ok", True)) if isinstance(meta, dict) else True,
|
||||
"sync_dt_ms": float(meta.get("sync_dt_ms", 0.0)) if isinstance(meta, dict) else 0.0,
|
||||
|
||||
"frame_id": int(meta.get("frame_id", 0) or 0),
|
||||
"async_packet_seq": int(
|
||||
capture_perf.get("async_packet_seq", 0) or 0
|
||||
),
|
||||
|
||||
"sync_ok": bool(meta.get("sync_ok", True)),
|
||||
"sync_dt_ms": float(meta.get("sync_dt_ms", 0.0) or 0.0),
|
||||
|
||||
"perf": {
|
||||
"origem_tensor": origem_tensor,
|
||||
"get_decoded_ms": t_get_decoded_ms,
|
||||
|
|
@ -689,7 +743,7 @@ class CameraMultispectral:
|
|||
# Saúde
|
||||
# ============================================================
|
||||
|
||||
def atualizar_saude(self):
|
||||
def atualizar_saude(self, pipeline_ia=None):
|
||||
#self.mostrar_log(f"[{self.mx_id}] Atualizando saude {self.dispositivo.name} multispectral...")
|
||||
|
||||
if self.imu is not None:
|
||||
|
|
@ -708,6 +762,17 @@ class CameraMultispectral:
|
|||
"velocidade": "",
|
||||
"sync_dt_ms": 0.0
|
||||
}
|
||||
|
||||
if pipeline_ia is not None:
|
||||
self._ultimo_pipeline_ia = dict(pipeline_ia)
|
||||
pipeline_ia = dict(self._ultimo_pipeline_ia or {})
|
||||
pipeline_ia_informado = True
|
||||
pipeline_ia_ok = (
|
||||
not pipeline_ia_informado
|
||||
or bool(pipeline_ia.get("ok", False))
|
||||
)
|
||||
performance["pipeline_ia"] = pipeline_ia
|
||||
|
||||
imu = self.imu.get_dados() if self.imu else {
|
||||
"valido": False,
|
||||
"calibrado": False,
|
||||
|
|
@ -820,8 +885,8 @@ class CameraMultispectral:
|
|||
saude += 50
|
||||
|
||||
if not frame_recente:
|
||||
motivos.append("Tensor antigo")
|
||||
saude -= 20
|
||||
motivos.append(f"Tensor sem atualização há {agora - ts_tensor:.2f}s")
|
||||
saude = 0
|
||||
|
||||
dur = float(resultado.get("duracao", 0.0) or 0.0)
|
||||
if dur > 1.5:
|
||||
|
|
@ -836,17 +901,34 @@ class CameraMultispectral:
|
|||
saude -= 10
|
||||
motivos.append(f"Sincronismo alto: {sync_dt:.1f} ms")
|
||||
|
||||
|
||||
if pipeline_ia_informado and not pipeline_ia_ok:
|
||||
motivos_pipeline = pipeline_ia.get("motivos", []) or []
|
||||
for motivo in motivos_pipeline:
|
||||
texto = f"Pipeline IA: {motivo}"
|
||||
if texto not in motivos:
|
||||
motivos.append(texto)
|
||||
saude = 0
|
||||
|
||||
saude = min(max(saude, 0), 100)
|
||||
|
||||
status_mod = StatusModulo.OPERANTE
|
||||
if not conectado:
|
||||
status_mod = StatusModulo.DESCONECTADO
|
||||
elif not frame_recente:
|
||||
status_mod = StatusModulo.FALHA
|
||||
elif not pipeline_ia_ok:
|
||||
status_mod = StatusModulo.FALHA
|
||||
elif saude <= 0:
|
||||
status_mod = StatusModulo.FALHA
|
||||
elif saude < 80:
|
||||
status_mod = StatusModulo.ALERTA
|
||||
|
||||
self.rodando = conectado and frame_recente
|
||||
self.rodando = (
|
||||
conectado
|
||||
and frame_recente
|
||||
and pipeline_ia_ok
|
||||
)
|
||||
|
||||
saude_geral = {
|
||||
"timestamp": agora,
|
||||
|
|
|
|||
|
|
@ -749,115 +749,132 @@ class OakFcc3Manager:
|
|||
if self.running:
|
||||
return
|
||||
|
||||
self.dev_info = self._resolve_device_info()
|
||||
self.mx_id = self._device_id_from_info(self.dev_info) or self.mx_id
|
||||
thread_anterior = self._capture_thread
|
||||
|
||||
self.device = dai.Device(self.dev_info)
|
||||
self.pipeline = dai.Pipeline()
|
||||
if (thread_anterior is not None and thread_anterior.is_alive()):
|
||||
raise RuntimeError("Não é seguro iniciar novo pipeline: thread OakFcc3AsyncCapture anterior ainda está viva.")
|
||||
|
||||
features = self.device.getConnectedCameraFeatures()
|
||||
try:
|
||||
self.dev_info = self._resolve_device_info()
|
||||
self.mx_id = self._device_id_from_info(self.dev_info) or self.mx_id
|
||||
|
||||
self.queues.clear()
|
||||
self.buffers.clear()
|
||||
self.camera_info.clear()
|
||||
self.control_queues.clear()
|
||||
self._last_raw_dims.clear()
|
||||
self.aligned_geometry = None
|
||||
self.device = dai.Device(self.dev_info)
|
||||
self.pipeline = dai.Pipeline()
|
||||
|
||||
if self._is_multispec_mode():
|
||||
self._start_multispec_pipeline(features)
|
||||
else:
|
||||
for f in features:
|
||||
socket = f.socket
|
||||
socket_name = socket.name
|
||||
if self.only_camera is not None and socket_name != self.only_camera:
|
||||
continue
|
||||
features = self.device.getConnectedCameraFeatures()
|
||||
|
||||
role = self.roles.get(socket_name, "unknown")
|
||||
self.queues.clear()
|
||||
self.buffers.clear()
|
||||
self.camera_info.clear()
|
||||
self.control_queues.clear()
|
||||
self._last_raw_dims.clear()
|
||||
self.aligned_geometry = None
|
||||
|
||||
print(f"[OAK] Criando câmera {socket_name} sensor={f.sensorName} role={role}")
|
||||
if self._is_multispec_mode():
|
||||
self._start_multispec_pipeline(features)
|
||||
else:
|
||||
for f in features:
|
||||
socket = f.socket
|
||||
socket_name = socket.name
|
||||
if self.only_camera is not None and socket_name != self.only_camera:
|
||||
continue
|
||||
|
||||
cam, output = self._create_camera_node_classic(
|
||||
socket=socket,
|
||||
sensor_name=f.sensorName,
|
||||
role=role,
|
||||
)
|
||||
role = self.roles.get(socket_name, "unknown")
|
||||
|
||||
self.apply_initial_camera_controls_to_node(cam, socket_name)
|
||||
print(f"[OAK] Criando câmera {socket_name} sensor={f.sensorName} role={role}")
|
||||
|
||||
xin_ctrl = self.pipeline.create(dai.node.XLinkIn)
|
||||
xin_ctrl.setStreamName(f"{socket_name}_ctrl")
|
||||
xin_ctrl.out.link(cam.inputControl)
|
||||
cam, output = self._create_camera_node_classic(
|
||||
socket=socket,
|
||||
sensor_name=f.sensorName,
|
||||
role=role,
|
||||
)
|
||||
|
||||
xout = self.pipeline.create(dai.node.XLinkOut)
|
||||
xout.setStreamName(socket_name)
|
||||
output.link(xout.input)
|
||||
self.apply_initial_camera_controls_to_node(cam, socket_name)
|
||||
|
||||
cam_id = socket_name
|
||||
xin_ctrl = self.pipeline.create(dai.node.XLinkIn)
|
||||
xin_ctrl.setStreamName(f"{socket_name}_ctrl")
|
||||
xin_ctrl.out.link(cam.inputControl)
|
||||
|
||||
self.queues[cam_id] = None
|
||||
self.buffers[cam_id] = deque(maxlen=self.buffer_size)
|
||||
self.control_queues[cam_id] = None
|
||||
xout = self.pipeline.create(dai.node.XLinkOut)
|
||||
xout.setStreamName(socket_name)
|
||||
output.link(xout.input)
|
||||
|
||||
self.camera_info[cam_id] = {
|
||||
"id": cam_id,
|
||||
"socket": socket_name,
|
||||
"sensor": f.sensorName,
|
||||
"role": role,
|
||||
}
|
||||
cam_id = socket_name
|
||||
|
||||
self._validate_capture_mode()
|
||||
self.queues[cam_id] = None
|
||||
self.buffers[cam_id] = deque(maxlen=self.buffer_size)
|
||||
self.control_queues[cam_id] = None
|
||||
|
||||
self._create_imu_node(self.pipeline)
|
||||
self.camera_info[cam_id] = {
|
||||
"id": cam_id,
|
||||
"socket": socket_name,
|
||||
"sensor": f.sensorName,
|
||||
"role": role,
|
||||
}
|
||||
|
||||
self.device.startPipeline(self.pipeline)
|
||||
self._validate_capture_mode()
|
||||
|
||||
self.q_imu = None
|
||||
self.tem_imu = False
|
||||
self._create_imu_node(self.pipeline)
|
||||
|
||||
if self.has_imu_pipeline:
|
||||
try:
|
||||
self.q_imu = self.device.getOutputQueue(
|
||||
name="imu",
|
||||
maxSize=8,
|
||||
self.device.startPipeline(self.pipeline)
|
||||
|
||||
self.q_imu = None
|
||||
self.tem_imu = False
|
||||
|
||||
if self.has_imu_pipeline:
|
||||
try:
|
||||
self.q_imu = self.device.getOutputQueue(
|
||||
name="imu",
|
||||
maxSize=8,
|
||||
blocking=False,
|
||||
)
|
||||
self.tem_imu = True
|
||||
print("[OAK] Fila IMU criada | maxSize=8")
|
||||
except Exception as e:
|
||||
self.q_imu = None
|
||||
self.tem_imu = False
|
||||
print(f"[WARN] Fila IMU indisponível: {e}")
|
||||
|
||||
for cam_id in self.camera_info.keys():
|
||||
self.queues[cam_id] = self.device.getOutputQueue(
|
||||
name=cam_id,
|
||||
maxSize=self.buffer_size,
|
||||
blocking=False,
|
||||
)
|
||||
self.tem_imu = True
|
||||
print("[OAK] Fila IMU criada | maxSize=8")
|
||||
except Exception as e:
|
||||
self.q_imu = None
|
||||
self.tem_imu = False
|
||||
print(f"[WARN] Fila IMU indisponível: {e}")
|
||||
|
||||
for cam_id in self.camera_info.keys():
|
||||
self.queues[cam_id] = self.device.getOutputQueue(
|
||||
name=cam_id,
|
||||
maxSize=self.buffer_size,
|
||||
blocking=False,
|
||||
)
|
||||
self.control_queues[cam_id] = self.device.getInputQueue(
|
||||
name=f"{cam_id}_ctrl",
|
||||
maxSize=4,
|
||||
blocking=False,
|
||||
)
|
||||
|
||||
self.control_queues[cam_id] = self.device.getInputQueue(
|
||||
name=f"{cam_id}_ctrl",
|
||||
maxSize=4,
|
||||
blocking=False,
|
||||
)
|
||||
self.running = True
|
||||
|
||||
self.running = True
|
||||
if not hasattr(self, "async_capture_enabled"):
|
||||
self.async_capture_enabled = True
|
||||
if not hasattr(self, "async_capture_mode"):
|
||||
self.async_capture_mode = "latest"
|
||||
if not hasattr(self, "async_capture_max_queue"):
|
||||
self.async_capture_max_queue = 2
|
||||
|
||||
if not hasattr(self, "async_capture_enabled"):
|
||||
self.async_capture_enabled = True
|
||||
if not hasattr(self, "async_capture_mode"):
|
||||
self.async_capture_mode = "latest"
|
||||
if not hasattr(self, "async_capture_max_queue"):
|
||||
self.async_capture_max_queue = 2
|
||||
self._reset_async_capture_state()
|
||||
self._start_async_capture_thread()
|
||||
time.sleep(0.05)
|
||||
self._reset_async_capture_state()
|
||||
self._start_async_capture_thread()
|
||||
time.sleep(0.05)
|
||||
|
||||
except Exception:
|
||||
# Garante limpeza mesmo se a falha acontecer
|
||||
# no meio da construção do pipeline.
|
||||
try:
|
||||
self.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise
|
||||
|
||||
def stop(self):
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
# Impede novas capturas imediatamente.
|
||||
self.running = False
|
||||
|
||||
self._stop_async_capture_thread()
|
||||
|
||||
try:
|
||||
|
|
@ -874,16 +891,25 @@ class OakFcc3Manager:
|
|||
|
||||
self.pipeline = None
|
||||
self.device = None
|
||||
|
||||
self.queues.clear()
|
||||
self.buffers.clear()
|
||||
self.camera_info.clear()
|
||||
self.control_queues.clear()
|
||||
self._last_raw_dims.clear()
|
||||
|
||||
self.aligned_geometry = None
|
||||
self.q_imu = None
|
||||
self.tem_imu = False
|
||||
self.has_imu_pipeline = False
|
||||
self.running = False
|
||||
|
||||
try:
|
||||
with self._capture_cond:
|
||||
self._latest_packet = None
|
||||
self._packet_queue.clear()
|
||||
self._capture_cond.notify_all()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _is_fatal_depthai_error(self, erro):
|
||||
txt = str(erro)
|
||||
|
|
@ -1734,8 +1760,28 @@ class OakFcc3Manager:
|
|||
self._capture_cond.notify_all()
|
||||
|
||||
th = getattr(self, "_capture_thread", None)
|
||||
if th is not None and th.is_alive():
|
||||
if (
|
||||
th is not None
|
||||
and th.is_alive()
|
||||
and th is not threading.current_thread()
|
||||
):
|
||||
th.join(timeout=1.0)
|
||||
|
||||
if th.is_alive():
|
||||
try:
|
||||
if self.device is not None:
|
||||
self.device.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
th.join(timeout=2.0)
|
||||
|
||||
if th.is_alive():
|
||||
self._capture_thread_stats["last_error"] = ("thread de captura não encerrou após fechamento do device")
|
||||
return False
|
||||
|
||||
self._capture_thread = None
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -1743,6 +1789,9 @@ class OakFcc3Manager:
|
|||
|
||||
def _async_capture_loop(self):
|
||||
if not hasattr(self, "_capture_thread_stats"):
|
||||
thread_anterior = self._capture_thread
|
||||
if (thread_anterior is not None and thread_anterior.is_alive()):
|
||||
raise RuntimeError("Não é seguro iniciar novo pipeline: thread OakFcc3AsyncCapture anterior ainda está viva.")
|
||||
self._reset_async_capture_state()
|
||||
|
||||
self._capture_thread_stats["started"] = True
|
||||
|
|
|
|||
|
|
@ -113,6 +113,14 @@ class CameraManager:
|
|||
self._tensor_ts = 0.0
|
||||
self._tensor_consumido_ts = 0.0
|
||||
|
||||
self.pipeline_ia_ok = False
|
||||
self._ultimo_tensor_ok_ts = 0.0
|
||||
self._ultima_inferencia_ok_ts = 0.0
|
||||
self._ultima_deteccao_ok_ts = 0.0
|
||||
self._ultima_saude_pipeline_ia = {}
|
||||
self._falhas_captura_consecutivas = 0
|
||||
self._primeira_falha_captura_ts = 0.0
|
||||
|
||||
self._pred_cache = {
|
||||
"ts": 0.0,
|
||||
"tensor_ts": 0.0,
|
||||
|
|
@ -189,14 +197,22 @@ class CameraManager:
|
|||
}
|
||||
|
||||
def inicializar(self, mx_id):
|
||||
if self.iniciando:
|
||||
return
|
||||
|
||||
if mx_id is None:
|
||||
return
|
||||
return False
|
||||
|
||||
with self._vida_lock:
|
||||
if self.iniciando:
|
||||
return False
|
||||
|
||||
if (
|
||||
self.camera is not None
|
||||
and self.operante
|
||||
and self.pipeline_ia_ok
|
||||
):
|
||||
return True
|
||||
|
||||
self.iniciando = True
|
||||
self.pipeline_ia_ok = False
|
||||
|
||||
try:
|
||||
self.mx_id = mx_id
|
||||
|
|
@ -210,11 +226,9 @@ class CameraManager:
|
|||
|
||||
if self.camera is None:
|
||||
self.mostrar_log(f"❌ Camera com ID {mx_id} não iniciada.")
|
||||
return
|
||||
return False
|
||||
|
||||
self.mostrar_log(
|
||||
f"📷 Camera selecionada: {self.camera.modelo} - {self.camera.mx_id}"
|
||||
)
|
||||
self.mostrar_log(f"📷 Camera selecionada: {self.camera.modelo} - {self.camera.mx_id}")
|
||||
|
||||
self._reset_runtime_state()
|
||||
|
||||
|
|
@ -228,25 +242,41 @@ class CameraManager:
|
|||
self.operante = False
|
||||
self._em_warmup = True
|
||||
|
||||
warmup_ok = self._executar_warmup_camera_manager(
|
||||
n_tensors=5,
|
||||
n_infer=4,
|
||||
n_detector=2,
|
||||
timeout_s=8.0,
|
||||
)
|
||||
warmup_ok = self._executar_warmup_camera_manager(n_ciclos=3, timeout_s=12.0)
|
||||
|
||||
self._em_warmup = False
|
||||
|
||||
if not warmup_ok:
|
||||
self.mostrar_log(
|
||||
"[weed][WARMUP] finalizou com alerta, liberando operação mesmo assim"
|
||||
)
|
||||
self.mostrar_log("[weed][WARMUP] recuperação rejeitada: pipeline não concluiu validação ponta a ponta")
|
||||
self.fechar_camera_manager("warmup incompleto após reconexão")
|
||||
return False
|
||||
|
||||
self.operante = True
|
||||
|
||||
self._iniciar_loops_se_necessario(self.seg_config)
|
||||
|
||||
self.atualizar_saude_camera()
|
||||
# A avaliação final precisa acontecer depois que a inicialização acabou.
|
||||
self.iniciando = False
|
||||
|
||||
pipeline_ia = self._avaliar_saude_pipeline_ia()
|
||||
|
||||
if not pipeline_ia.get("ok", False):
|
||||
self.mostrar_log(
|
||||
"[weed] inicialização rejeitada após warmup: "
|
||||
+ "; ".join(pipeline_ia.get("motivos", []))
|
||||
)
|
||||
|
||||
self.fechar_camera_manager(motivo="pipeline IA inválido após warmup", camera_esperada=self.camera)
|
||||
return False
|
||||
|
||||
self.atualizar_saude_camera(pipeline_ia=pipeline_ia)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed] erro ao inicializar câmera: {e}")
|
||||
self.fechar_camera_manager(motivo=f"falha de inicialização: {e}", camera_esperada=self.camera)
|
||||
return False
|
||||
|
||||
finally:
|
||||
self.iniciando = False
|
||||
|
|
@ -325,37 +355,109 @@ class CameraManager:
|
|||
self._iniciar_loop_analise_continua(freq=freq_analise)
|
||||
self._loop_analise_iniciado = True
|
||||
|
||||
def fechar_camera_manager(self, motivo=""):
|
||||
with self._vida_lock:
|
||||
self._fechando_camera = True
|
||||
def _registrar_falha_captura(
|
||||
self,
|
||||
camera_atual,
|
||||
erro,
|
||||
):
|
||||
if camera_atual is not self.camera:
|
||||
return False
|
||||
|
||||
try:
|
||||
if self.camera is not None:
|
||||
self.camera.parar()
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed] Erro ao fechar câmera: {e}")
|
||||
agora = time.monotonic()
|
||||
|
||||
self._falhas_captura_consecutivas += 1
|
||||
|
||||
if self._primeira_falha_captura_ts <= 0:
|
||||
self._primeira_falha_captura_ts = agora
|
||||
|
||||
duracao_falha = (
|
||||
agora - self._primeira_falha_captura_ts
|
||||
)
|
||||
|
||||
try:
|
||||
fatal = camera_atual._is_erro_fatal_depthai(erro)
|
||||
except Exception:
|
||||
fatal = False
|
||||
|
||||
deve_reiniciar = (
|
||||
fatal
|
||||
or self._falhas_captura_consecutivas >= 5
|
||||
or duracao_falha >= 3.0
|
||||
)
|
||||
|
||||
if not deve_reiniciar:
|
||||
self.mostrar_log(
|
||||
f"[weed][CAPTURE] falha transitória "
|
||||
f"{self._falhas_captura_consecutivas}/5: {erro}"
|
||||
)
|
||||
return False
|
||||
|
||||
self.mostrar_log(
|
||||
f"[weed][CAPTURE] reiniciando câmera | "
|
||||
f"falhas={self._falhas_captura_consecutivas} "
|
||||
f"duração={duracao_falha:.2f}s "
|
||||
f"erro={erro}"
|
||||
)
|
||||
|
||||
self.fechar_camera_manager(
|
||||
motivo=f"watchdog de captura: {erro}",
|
||||
camera_esperada=camera_atual,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def fechar_camera_manager(self, motivo="", camera_esperada=None):
|
||||
with self._vida_lock:
|
||||
# Um erro atrasado da câmera antiga jamais pode fechar a nova.
|
||||
if (camera_esperada is not None and self.camera is not camera_esperada):
|
||||
self.mostrar_log("[weed] fechamento ignorado: a câmera ativa já foi substituída")
|
||||
return False
|
||||
|
||||
camera_alvo = self.camera
|
||||
|
||||
self.camera = None
|
||||
self.operante = False
|
||||
self._fechando_camera = False
|
||||
self.pipeline_ia_ok = False
|
||||
self._fechando_camera = True
|
||||
|
||||
self._reset_runtime_state()
|
||||
|
||||
try:
|
||||
self._forcar_bicos_off(motivo=f"câmera fechada: {motivo}", intervalo_min_s=0.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if camera_alvo is not None:
|
||||
camera_alvo.parar()
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed] Erro ao fechar câmera: {e}")
|
||||
|
||||
finally:
|
||||
self._fechando_camera = False
|
||||
|
||||
self.mostrar_log(f"[weed] Camera Manager fechado: {motivo}")
|
||||
|
||||
return True
|
||||
|
||||
# ============================================================
|
||||
# Warmup
|
||||
# ============================================================
|
||||
|
||||
def _executar_warmup_camera_manager(
|
||||
self,
|
||||
n_tensors=5,
|
||||
n_infer=4,
|
||||
n_detector=2,
|
||||
timeout_s=8.0,
|
||||
n_ciclos=3,
|
||||
timeout_s=12.0,
|
||||
):
|
||||
if self.camera is None:
|
||||
camera_alvo = self.camera
|
||||
|
||||
if camera_alvo is None:
|
||||
return False
|
||||
|
||||
self.mostrar_log("[weed][WARMUP] iniciando warmup do CameraManager...")
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] iniciando {n_ciclos} ciclos ponta a ponta..."
|
||||
)
|
||||
|
||||
operante_anterior = self.operante
|
||||
warmup_anterior = self._em_warmup
|
||||
|
|
@ -363,121 +465,178 @@ class CameraManager:
|
|||
self.operante = False
|
||||
self._em_warmup = True
|
||||
|
||||
tensor5 = None
|
||||
res = None
|
||||
predictions = None
|
||||
deadline = time.monotonic() + float(timeout_s)
|
||||
ultimo_frame_id = 0
|
||||
ciclos_ok = 0
|
||||
ultimo_ciclo = None
|
||||
|
||||
try:
|
||||
tensor_ok = 0
|
||||
t0 = time.time()
|
||||
while ciclos_ok < n_ciclos and time.monotonic() < deadline:
|
||||
if self.camera is not camera_alvo:
|
||||
self.mostrar_log(
|
||||
"[weed][WARMUP] câmera foi substituída durante o warmup"
|
||||
)
|
||||
return False
|
||||
|
||||
while time.time() - t0 < timeout_s and tensor_ok < n_tensors:
|
||||
try:
|
||||
# 1. Frame/tensor realmente novo
|
||||
t_cap0 = time.perf_counter()
|
||||
tensor5, res = self.camera.requisitar_tensor_multispec(force=True)
|
||||
|
||||
tensor5, res = camera_alvo.requisitar_tensor_multispec(
|
||||
force=True
|
||||
)
|
||||
|
||||
t_cap1 = time.perf_counter()
|
||||
|
||||
if tensor5 is not None:
|
||||
tensor_ok += 1
|
||||
|
||||
with self._lock_tensor:
|
||||
self._tensor_pronto = tensor5
|
||||
self._tensor_res = res
|
||||
self._tensor_ts = time.time()
|
||||
erro = res.get("erro") if isinstance(res, dict) else None
|
||||
|
||||
if tensor5 is None or erro:
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] tensor {tensor_ok}/{n_tensors} "
|
||||
f"total={(t_cap1 - t_cap0) * 1000.0:.1f}ms "
|
||||
f"shape={getattr(tensor5, 'shape', None)}"
|
||||
f"[weed][WARMUP] captura inválida: {erro}"
|
||||
)
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed][WARMUP] tensor falhou: {e}")
|
||||
frame_id = int(
|
||||
res.get("frame_id")
|
||||
or res.get("async_packet_seq")
|
||||
or 0
|
||||
)
|
||||
|
||||
time.sleep(0.03)
|
||||
if frame_id <= 0:
|
||||
self.mostrar_log(
|
||||
"[weed][WARMUP] frame sem identidade válida"
|
||||
)
|
||||
time.sleep(0.03)
|
||||
continue
|
||||
|
||||
if tensor5 is None:
|
||||
self.mostrar_log("[weed][WARMUP] abortado: sem tensor válido")
|
||||
return False
|
||||
if frame_id <= ultimo_frame_id:
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] frame não avançou: "
|
||||
f"anterior={ultimo_frame_id} atual={frame_id}"
|
||||
)
|
||||
time.sleep(0.03)
|
||||
continue
|
||||
|
||||
infer_ok = 0
|
||||
|
||||
for i in range(max(0, n_infer)):
|
||||
try:
|
||||
# 2. Inferência sobre este frame
|
||||
t_inf0 = time.perf_counter()
|
||||
|
||||
predictions = self.model_svc.infer_tensor_fast(
|
||||
tensor5,
|
||||
keep_probs=False,
|
||||
)
|
||||
|
||||
t_inf1 = time.perf_counter()
|
||||
|
||||
infer_full = getattr(self.model_svc, "_ultimo_predictions_full", {}) or {}
|
||||
if predictions is None:
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] inferência inválida no frame {frame_id}"
|
||||
)
|
||||
continue
|
||||
|
||||
infer_forward_ms = infer_full.get(
|
||||
"forward_ms",
|
||||
infer_full.get("infer_ms", None),
|
||||
infer_full = (
|
||||
getattr(
|
||||
self.model_svc,
|
||||
"_ultimo_predictions_full",
|
||||
{},
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
infer_ms = (t_inf1 - t_inf0) * 1000.0
|
||||
infer_gpu_ms = infer_full.get(
|
||||
"forward_ms",
|
||||
infer_full.get("infer_ms", 0.0),
|
||||
)
|
||||
infer_prepare_ms = infer_full.get("prepare_ms", 0.0)
|
||||
infer_post_ms = infer_full.get("post_ms", 0.0)
|
||||
|
||||
self._atualizar_fps_inferencia(
|
||||
infer_ms=(t_inf1 - t_inf0) * 1000.0,
|
||||
infer_gpu_ms=infer_forward_ms,
|
||||
infer_ms=infer_ms,
|
||||
infer_gpu_ms=infer_gpu_ms,
|
||||
)
|
||||
|
||||
if predictions is not None:
|
||||
infer_ok += 1
|
||||
self._ultimo_raw_base = tensor5
|
||||
self._ultimo_raw_input = tensor5
|
||||
self._ultimo_predictions = predictions
|
||||
self._ultimo_predictions_full = infer_full
|
||||
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] infer {i + 1}/{n_infer} "
|
||||
f"total={(t_inf1 - t_inf0) * 1000.0:.1f}ms "
|
||||
f"prep={infer_prepare_ms:.1f}ms "
|
||||
f"fwd={infer_forward_ms if infer_forward_ms is not None else -1:.1f}ms "
|
||||
f"post={infer_post_ms:.1f}ms"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed][WARMUP] infer falhou: {e}")
|
||||
|
||||
time.sleep(0.03)
|
||||
|
||||
if predictions is None:
|
||||
self.mostrar_log("[weed][WARMUP] sem predictions válidas")
|
||||
return False
|
||||
|
||||
det_ok = 0
|
||||
|
||||
for i in range(max(0, n_detector)):
|
||||
try:
|
||||
# 3. Detecção sobre a prediction deste mesmo frame
|
||||
t_det0 = time.perf_counter()
|
||||
analise_completa = self.detectar_ervas(predictions)
|
||||
t_det1 = time.perf_counter()
|
||||
|
||||
if isinstance(analise_completa, dict):
|
||||
det_ok += 1
|
||||
analise = analise_completa.get("dados_visuais", {})
|
||||
self._ultima_analise = analise_completa.copy()
|
||||
self._set_pub_cache("analise", analise)
|
||||
if not isinstance(analise_completa, dict):
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] detecção inválida no frame {frame_id}"
|
||||
)
|
||||
continue
|
||||
|
||||
agora = time.time()
|
||||
ciclos_ok += 1
|
||||
ultimo_frame_id = frame_id
|
||||
|
||||
ultimo_ciclo = {
|
||||
"tensor": tensor5,
|
||||
"res": res,
|
||||
"tensor_ts": agora,
|
||||
"predictions": predictions,
|
||||
"pred_ts": agora,
|
||||
"infer_full": infer_full,
|
||||
"analise": analise_completa,
|
||||
}
|
||||
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] detector {i + 1}/{n_detector} "
|
||||
f"total={(t_det1 - t_det0) * 1000.0:.1f}ms"
|
||||
f"[weed][WARMUP] ciclo {ciclos_ok}/{n_ciclos} OK | "
|
||||
f"frame_id={frame_id} "
|
||||
f"captura={(t_cap1 - t_cap0) * 1000.0:.1f}ms "
|
||||
f"infer={infer_ms:.1f}ms "
|
||||
f"det={(t_det1 - t_det0) * 1000.0:.1f}ms"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed][WARMUP] detector falhou: {e}")
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] ciclo falhou: "
|
||||
f"{type(e).__name__}: {e}"
|
||||
)
|
||||
time.sleep(0.05)
|
||||
|
||||
time.sleep(0.03)
|
||||
if ciclos_ok != n_ciclos or ultimo_ciclo is None:
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] rejeitado | "
|
||||
f"ciclos_ok={ciclos_ok}/{n_ciclos}"
|
||||
)
|
||||
return False
|
||||
|
||||
# Publica o estado nos caches internos, mas marca como já consumido.
|
||||
# Assim o warmup não gera uma análise operacional artificial.
|
||||
with self._lock_tensor:
|
||||
self._tensor_pronto = ultimo_ciclo["tensor"]
|
||||
self._tensor_res = ultimo_ciclo["res"]
|
||||
self._tensor_ts = ultimo_ciclo["tensor_ts"]
|
||||
self._tensor_consumido_ts = ultimo_ciclo["tensor_ts"]
|
||||
|
||||
with self._pred_lock:
|
||||
self._pred_cache = {
|
||||
"ts": ultimo_ciclo["pred_ts"],
|
||||
"tensor_ts": ultimo_ciclo["tensor_ts"],
|
||||
"predictions": ultimo_ciclo["predictions"],
|
||||
"res": ultimo_ciclo["res"],
|
||||
"infer_ms": 0.0,
|
||||
"infer_gpu_ms": 0.0,
|
||||
"infer_prepare_ms": 0.0,
|
||||
"infer_post_ms": 0.0,
|
||||
"fps_model": float(self._fps_infer_ema or 0.0),
|
||||
}
|
||||
self._pred_consumido_ts = ultimo_ciclo["pred_ts"]
|
||||
|
||||
self._ultimo_raw_base = ultimo_ciclo["tensor"]
|
||||
self._ultimo_raw_input = ultimo_ciclo["tensor"]
|
||||
self._ultimo_predictions = ultimo_ciclo["predictions"]
|
||||
self._ultimo_predictions_full = ultimo_ciclo["infer_full"]
|
||||
self._ultima_analise = ultimo_ciclo["analise"].copy()
|
||||
|
||||
agora = time.time()
|
||||
self._ultimo_tensor_ok_ts = agora
|
||||
self._ultima_inferencia_ok_ts = agora
|
||||
self._ultima_deteccao_ok_ts = agora
|
||||
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] finalizado | "
|
||||
f"tensor_ok={tensor_ok}/{n_tensors} "
|
||||
f"infer_ok={infer_ok}/{n_infer} "
|
||||
f"det_ok={det_ok}/{n_detector}"
|
||||
f"[weed][WARMUP] aprovado | "
|
||||
f"{n_ciclos} frames distintos processados ponta a ponta"
|
||||
)
|
||||
|
||||
return True
|
||||
|
|
@ -491,6 +650,7 @@ class CameraManager:
|
|||
self._em_warmup = warmup_anterior
|
||||
|
||||
self.perf = VisualPerfMonitor(janela=180)
|
||||
|
||||
if self.camera is not None:
|
||||
self.camera.perf = self.perf
|
||||
|
||||
|
|
@ -498,25 +658,19 @@ class CameraManager:
|
|||
# Saúde / config dinâmica
|
||||
# ============================================================
|
||||
|
||||
def atualizar_saude_camera(self):
|
||||
def atualizar_saude_camera(self, pipeline_ia=None):
|
||||
self._ultima_saude_ts = time.time()
|
||||
|
||||
try:
|
||||
if self.camera is not None:
|
||||
self.camera.atualizar_saude()
|
||||
if pipeline_ia is None:
|
||||
pipeline_ia = self._avaliar_saude_pipeline_ia()
|
||||
|
||||
self.camera.atualizar_saude(pipeline_ia=pipeline_ia)
|
||||
|
||||
elif self.mx_id is not None:
|
||||
from camera_worker.manager import definir_saude_camera
|
||||
|
||||
definir_saude_camera(
|
||||
self.mx_id,
|
||||
StatusModulo.DESCONECTADO,
|
||||
0,
|
||||
["desconectado"],
|
||||
False,
|
||||
{},
|
||||
disp=T_Code.Cam,
|
||||
conectado=False,
|
||||
)
|
||||
definir_saude_camera(self.mx_id, StatusModulo.DESCONECTADO, 0, ["desconectado"], False, {}, disp=T_Code.Cam, conectado=False)
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[saude] erro: {e}")
|
||||
|
|
@ -554,6 +708,75 @@ class CameraManager:
|
|||
except Exception as e:
|
||||
self.mostrar_log(f"[weed] erro ao atualizar config dinâmica: {e}")
|
||||
|
||||
def _avaliar_saude_pipeline_ia(self):
|
||||
agora = time.time()
|
||||
|
||||
cfg = self.seg_config or {}
|
||||
|
||||
timeout_tensor = float(
|
||||
cfg.get("watchdog_tensor_timeout_s", 2.0)
|
||||
)
|
||||
timeout_inferencia = float(
|
||||
cfg.get("watchdog_inferencia_timeout_s", 2.0)
|
||||
)
|
||||
timeout_deteccao = float(
|
||||
cfg.get("watchdog_deteccao_timeout_s", 2.0)
|
||||
)
|
||||
|
||||
def idade(ts):
|
||||
if not ts:
|
||||
return float("inf")
|
||||
return max(0.0, agora - float(ts))
|
||||
|
||||
idade_tensor = idade(self._ultimo_tensor_ok_ts)
|
||||
idade_inferencia = idade(self._ultima_inferencia_ok_ts)
|
||||
idade_deteccao = idade(self._ultima_deteccao_ok_ts)
|
||||
|
||||
motivos = []
|
||||
|
||||
if self.camera is None:
|
||||
motivos.append("câmera indisponível")
|
||||
elif self.iniciando:
|
||||
motivos.append("câmera inicializando")
|
||||
elif self._em_warmup:
|
||||
motivos.append("pipeline em warmup")
|
||||
elif not self.operante:
|
||||
motivos.append("CameraManager não operante")
|
||||
else:
|
||||
if idade_tensor > timeout_tensor:
|
||||
motivos.append(
|
||||
f"tensor antigo: {idade_tensor:.2f}s"
|
||||
)
|
||||
|
||||
if idade_inferencia > timeout_inferencia:
|
||||
motivos.append(
|
||||
f"inferência antiga: {idade_inferencia:.2f}s"
|
||||
)
|
||||
|
||||
if idade_deteccao > timeout_deteccao:
|
||||
motivos.append(
|
||||
f"detecção antiga: {idade_deteccao:.2f}s"
|
||||
)
|
||||
|
||||
ok = len(motivos) == 0
|
||||
|
||||
resultado = {
|
||||
"ok": ok,
|
||||
"timestamp": agora,
|
||||
"motivos": motivos,
|
||||
"idade_tensor_s": idade_tensor,
|
||||
"idade_inferencia_s": idade_inferencia,
|
||||
"idade_deteccao_s": idade_deteccao,
|
||||
"timeout_tensor_s": timeout_tensor,
|
||||
"timeout_inferencia_s": timeout_inferencia,
|
||||
"timeout_deteccao_s": timeout_deteccao,
|
||||
}
|
||||
|
||||
self.pipeline_ia_ok = ok
|
||||
self._ultima_saude_pipeline_ia = resultado
|
||||
|
||||
return resultado
|
||||
|
||||
# ============================================================
|
||||
# Caches
|
||||
# ============================================================
|
||||
|
|
@ -889,55 +1112,92 @@ class CameraManager:
|
|||
|
||||
def _iniciar_loop_captura_tensor(self, freq=25.0):
|
||||
def loop():
|
||||
periodo = 1.0 / max(float(freq), 0.1)
|
||||
|
||||
while True:
|
||||
t0_wall = time.time()
|
||||
t0 = time.perf_counter()
|
||||
t0_perf = time.perf_counter()
|
||||
camera_atual = None
|
||||
|
||||
try:
|
||||
if self.camera is None:
|
||||
time.sleep(1.0)
|
||||
if (self.iniciando or self._em_warmup or not self.operante or self.camera is None):
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
tensor5, res = self.camera.requisitar_tensor_multispec(force=True)
|
||||
camera_atual = self.camera
|
||||
|
||||
if isinstance(res, dict) and res.get("erro"):
|
||||
erro = res.get("erro")
|
||||
if self.camera._is_erro_fatal_depthai(erro):
|
||||
self.fechar_camera_manager(f"falha fatal DepthAI: {erro}")
|
||||
return
|
||||
tensor5, res = (camera_atual.requisitar_tensor_multispec(force=True))
|
||||
|
||||
if tensor5 is not None:
|
||||
agora = time.time()
|
||||
perf = res.get("perf", {}) if isinstance(res, dict) else {}
|
||||
erro = (res.get("erro") if isinstance(res, dict) else None)
|
||||
|
||||
with self._lock_tensor:
|
||||
self._tensor_pronto = tensor5
|
||||
self._tensor_res = res
|
||||
self._tensor_ts = agora
|
||||
if erro:
|
||||
reiniciou = self._registrar_falha_captura(camera_atual, erro)
|
||||
|
||||
t1 = time.perf_counter()
|
||||
time.sleep(0.50 if reiniciou else 0.05)
|
||||
continue
|
||||
|
||||
self.perf.tick(
|
||||
"tensor",
|
||||
latencia_ms=(t1 - t0) * 1000.0,
|
||||
tensor_core_ms=float(perf.get("total_ms", 0.0) or 0.0),
|
||||
get_decoded_ms=float(perf.get("get_decoded_ms", 0.0) or 0.0),
|
||||
build_ms=float(perf.get("build_ms", 0.0) or 0.0),
|
||||
validate_ms=float(perf.get("validate_ms", 0.0) or 0.0),
|
||||
post_ms=float(perf.get("post_ms", 0.0) or 0.0),
|
||||
preview_ms=float(perf.get("preview_ms", 0.0) or 0.0),
|
||||
frame_ts=agora,
|
||||
idade_frame_ms=0.0,
|
||||
)
|
||||
if tensor5 is None:
|
||||
self.perf.inc("tensor_none")
|
||||
|
||||
reiniciou = self._registrar_falha_captura(camera_atual, "tensor None sem erro explícito")
|
||||
|
||||
time.sleep(0.50 if reiniciou else 0.05)
|
||||
continue
|
||||
|
||||
# Uma captura válida limpa a sequência de falhas.
|
||||
self._falhas_captura_consecutivas = 0
|
||||
self._primeira_falha_captura_ts = 0.0
|
||||
|
||||
agora = time.time()
|
||||
perf = (
|
||||
res.get("perf", {})
|
||||
if isinstance(res, dict)
|
||||
else {}
|
||||
)
|
||||
|
||||
with self._lock_tensor:
|
||||
self._tensor_pronto = tensor5
|
||||
self._tensor_res = res
|
||||
self._tensor_ts = agora
|
||||
|
||||
self._ultimo_tensor_ok_ts = agora
|
||||
|
||||
t1_perf = time.perf_counter()
|
||||
|
||||
self.perf.tick(
|
||||
"tensor",
|
||||
latencia_ms=(t1_perf - t0_perf) * 1000.0,
|
||||
tensor_core_ms=float(perf.get("total_ms", 0.0) or 0.0),
|
||||
get_decoded_ms=float(perf.get("get_decoded_ms", 0.0) or 0.0),
|
||||
build_ms=float(perf.get("build_ms", 0.0) or 0.0),
|
||||
validate_ms=float(perf.get("validate_ms", 0.0) or 0.0),
|
||||
post_ms=float(perf.get("post_ms", 0.0) or 0.0),
|
||||
preview_ms=float(perf.get("preview_ms", 0.0) or 0.0),
|
||||
frame_ts=agora,
|
||||
idade_frame_ms=0.0,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[CAPTURE] erro: {e}")
|
||||
self.mostrar_log(f"[weed][CAPTURE] Erro inesperado: {type(e).__name__}: {e}")
|
||||
|
||||
if camera_atual is None:
|
||||
camera_atual = self.camera
|
||||
|
||||
reiniciou = False
|
||||
|
||||
if camera_atual is not None:
|
||||
reiniciou = self._registrar_falha_captura(camera_atual, e)
|
||||
|
||||
time.sleep(0.50 if reiniciou else 0.05)
|
||||
|
||||
finally:
|
||||
lat = time.time() - t0_wall
|
||||
time.sleep(max(0.0, (1.0 / freq) - lat))
|
||||
gasto = time.time() - t0_wall
|
||||
restante = periodo - gasto
|
||||
|
||||
threading.Thread(target=loop, daemon=True).start()
|
||||
if restante > 0:
|
||||
time.sleep(restante)
|
||||
|
||||
threading.Thread(target=loop, name="WeedTensorCapture", daemon=True).start()
|
||||
|
||||
def _iniciar_loop_inferencia(self, freq=25.0):
|
||||
def loop():
|
||||
|
|
@ -1010,6 +1270,7 @@ class CameraManager:
|
|||
"infer_post_ms": float(infer_post_ms or 0.0),
|
||||
"fps_model": float(self._fps_infer_ema or 0.0),
|
||||
}
|
||||
self._ultima_inferencia_ok_ts = pred_ts
|
||||
|
||||
self._ultimo_raw_base = tensor5
|
||||
self._ultimo_raw_input = tensor5
|
||||
|
|
@ -1116,7 +1377,7 @@ class CameraManager:
|
|||
"infer_ms": pred_cache.get("infer_ms", 0.0),
|
||||
"infer_gpu_ms": pred_cache.get("infer_gpu_ms", 0.0),
|
||||
"analise": analise_convertida,
|
||||
})
|
||||
}, ts_origem=pred_ts)
|
||||
t_pub_analise1 = time.perf_counter()
|
||||
|
||||
# ====================================================
|
||||
|
|
@ -1165,6 +1426,9 @@ class CameraManager:
|
|||
|
||||
self._ultima_analise = analise_completa.copy()
|
||||
|
||||
# Só considera o ciclo válido quando análise, gate e comando já foram preparados com sucesso.
|
||||
self._ultima_deteccao_ok_ts = time.time()
|
||||
|
||||
t_loop1 = time.perf_counter()
|
||||
total_ms = (t_loop1 - t_loop0) * 1000.0
|
||||
self._ultimo_loop_analise_fps = 1000.0 / max(total_ms, 1e-6)
|
||||
|
|
@ -1360,52 +1624,81 @@ class CameraManager:
|
|||
|
||||
def _iniciar_loop_analise_continua(self, freq=15.0):
|
||||
def loop():
|
||||
ultimo_health_ts = 0.0
|
||||
ultimo_perf_ts = 0.0
|
||||
|
||||
while True:
|
||||
t0 = time.time()
|
||||
|
||||
try:
|
||||
if self.camera is None:
|
||||
time.sleep(0.5)
|
||||
camera_atual = self.camera
|
||||
|
||||
if camera_atual is None:
|
||||
self.pipeline_ia_ok = False
|
||||
self._garantir_bicos_off_se_nao_permitido(intervalo_min_s=0.20)
|
||||
time.sleep(0.20)
|
||||
continue
|
||||
|
||||
self._verificar_desconexao_camera()
|
||||
|
||||
agora = time.time()
|
||||
if not hasattr(self, "_ultimo_perf_publish"):
|
||||
self._ultimo_perf_publish = 0.0
|
||||
|
||||
if agora - self._ultimo_perf_publish >= 1.0:
|
||||
self._ultimo_perf_publish = agora
|
||||
# Saúde completa a cada 500 ms.
|
||||
if agora - ultimo_health_ts >= 0.50:
|
||||
ultimo_health_ts = agora
|
||||
|
||||
pipeline_ia = self._avaliar_saude_pipeline_ia()
|
||||
|
||||
if not pipeline_ia.get("ok", False):
|
||||
self._forcar_bicos_off(
|
||||
motivo=(
|
||||
"pipeline IA inválido: "
|
||||
+ "; ".join(
|
||||
pipeline_ia.get("motivos", [])
|
||||
)
|
||||
),
|
||||
intervalo_min_s=0.20,
|
||||
)
|
||||
|
||||
self.atualizar_saude_camera(pipeline_ia=pipeline_ia)
|
||||
|
||||
if agora - ultimo_perf_ts >= 1.0:
|
||||
ultimo_perf_ts = agora
|
||||
self._publicar_e_logar_performance()
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"Erro no loop supervisor weed: {e}")
|
||||
|
||||
try:
|
||||
self._forcar_bicos_off(motivo="erro no supervisor do Weed Worker", intervalo_min_s=0.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
finally:
|
||||
lat = time.time() - t0
|
||||
time.sleep(max(0.0, (1.0 / freq) - lat))
|
||||
time.sleep(max(0.0, (1.0 / max(freq, 0.1)) - lat))
|
||||
|
||||
threading.Thread(target=loop, daemon=True).start()
|
||||
threading.Thread(target=loop, name="WeedSupervisor", daemon=True).start()
|
||||
|
||||
def _verificar_desconexao_camera(self):
|
||||
status = StatusModulo(
|
||||
(self.camera.ultima_saude or {}).get(
|
||||
"status",
|
||||
StatusModulo.DESCONECTADO.value,
|
||||
)
|
||||
)
|
||||
camera_atual = self.camera
|
||||
|
||||
ts_status = (self.camera.ultima_saude or {}).get("timestamp", 0)
|
||||
if camera_atual is None:
|
||||
return
|
||||
|
||||
if status == StatusModulo.DESCONECTADO and (time.time() - ts_status) > 10.0:
|
||||
try:
|
||||
if getattr(self.camera, "imu", None):
|
||||
self.camera.imu.parar()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
status = StatusModulo((camera_atual.ultima_saude or {}).get("status", StatusModulo.DESCONECTADO.value))
|
||||
except Exception:
|
||||
status = StatusModulo.DESCONECTADO
|
||||
|
||||
self.camera = None
|
||||
self.operante = False
|
||||
ts_status = float((camera_atual.ultima_saude or {}).get("timestamp", 0.0) or 0.0)
|
||||
|
||||
if (
|
||||
status == StatusModulo.DESCONECTADO
|
||||
and ts_status > 0
|
||||
and (time.time() - ts_status) > 10.0
|
||||
):
|
||||
self.fechar_camera_manager(motivo="câmera permaneceu desconectada por mais de 10s", camera_esperada=camera_atual)
|
||||
|
||||
# ============================================================
|
||||
# Detecção / atuação
|
||||
|
|
@ -1426,15 +1719,19 @@ class CameraManager:
|
|||
# Publicação / cache Redis
|
||||
# ============================================================
|
||||
|
||||
def _set_pub_cache(self, chave, valor):
|
||||
def _set_pub_cache(self, chave, valor, ts_origem=None):
|
||||
try:
|
||||
agora = time.time()
|
||||
ts_origem = agora if ts_origem is None else float(ts_origem)
|
||||
|
||||
with self._pub_lock:
|
||||
self._pub_cache[chave] = valor
|
||||
self._pub_cache[f"ts_{chave}"] = agora
|
||||
self._pub_cache[f"ts_{chave}"] = ts_origem
|
||||
self._pub_cache[f"dirty_{chave}"] = True
|
||||
self._pub_cache["ts_analise"] = agora
|
||||
|
||||
# Só uma análise real pode avançar ts_analise.
|
||||
if chave == "analise":
|
||||
self._pub_cache["ts_analise"] = ts_origem
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed] erro ao atualizar pub_cache[{chave}]: {e}")
|
||||
|
|
@ -1444,7 +1741,8 @@ class CameraManager:
|
|||
cache = dict(self._pub_cache)
|
||||
|
||||
payload_weed = {
|
||||
"ts_analise": time.time(),
|
||||
"ts_publicacao": time.time(),
|
||||
"ts_analise": float(cache.get("ts_analise", 0.0) or 0.0),
|
||||
}
|
||||
|
||||
payload_controle = None
|
||||
|
|
@ -1634,10 +1932,11 @@ class CameraManager:
|
|||
)
|
||||
|
||||
camera_ok = (
|
||||
self.camera is not None and
|
||||
self.operante and
|
||||
not self.iniciando and
|
||||
not self._em_warmup
|
||||
self.camera is not None
|
||||
and self.operante
|
||||
and self.pipeline_ia_ok
|
||||
and not self.iniciando
|
||||
and not self._em_warmup
|
||||
)
|
||||
|
||||
retornando_base = (
|
||||
|
|
@ -2177,5 +2476,4 @@ class CameraManager:
|
|||
|
||||
def _finalizar_salvamento_posprocessamento(self):
|
||||
with self._posproc_save_lock:
|
||||
self._posproc_salvando = False
|
||||
|
||||
self._posproc_salvando = False
|
||||
Loading…
Reference in New Issue