robustez no camera manager do weed e multiesctral
This commit is contained in:
parent
a59bee710d
commit
4ee8a1e76a
|
|
@ -83,6 +83,7 @@ class CameraMultispectral:
|
|||
|
||||
self._vida_lock = threading.RLock()
|
||||
self._parando = False
|
||||
self._falha_fatal_pendente = False
|
||||
self.client = None
|
||||
|
||||
self._ultimo_resultado_tensor = {
|
||||
|
|
@ -452,15 +453,22 @@ class CameraMultispectral:
|
|||
pass
|
||||
|
||||
def _falha_fatal_depthai(self, motivo):
|
||||
self.mostrar_log(f"[CameraMultispectral] Falha fatal DepthAI. Fechando pipeline: {motivo}")
|
||||
"""
|
||||
Marca a falha, mas não fecha o dai.Device nesta camada.
|
||||
|
||||
O CameraManager é o único dono do ciclo de vida e fará o fechamento.
|
||||
Isso evita CameraMultispectral.parar() e CameraManager.fechar_camera_manager()
|
||||
concorrendo sobre o mesmo objeto nativo.
|
||||
"""
|
||||
if self._falha_fatal_pendente:
|
||||
return
|
||||
|
||||
self._falha_fatal_pendente = True
|
||||
self.mostrar_log(
|
||||
f"[CameraMultispectral] Falha fatal DepthAI detectada: {motivo}"
|
||||
)
|
||||
self._marcar_desconectada(motivo)
|
||||
|
||||
try:
|
||||
self.parar()
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[CameraMultispectral] Erro ao parar após falha fatal: {e}")
|
||||
|
||||
# ============================================================
|
||||
# Captura principal
|
||||
# ============================================================
|
||||
|
|
@ -567,6 +575,7 @@ class CameraMultispectral:
|
|||
t_preview_ms = 0.0
|
||||
|
||||
ts = time.time()
|
||||
ts_raw_perf = time.perf_counter()
|
||||
dur = time.perf_counter() - t_total0
|
||||
|
||||
meta = dict(meta or {})
|
||||
|
|
@ -606,6 +615,24 @@ class CameraMultispectral:
|
|||
self.timestamp_ultimo_tensor = ts
|
||||
self._ultimo_resultado_tensor = resultado
|
||||
|
||||
# O mesmo pacote RAW usado na inferência alimenta o cache científico.
|
||||
# O salvamento não precisa consumir outra vez o cursor DepthAI.
|
||||
if isinstance(frame, dict) and frame:
|
||||
# Arrays do pacote são imutáveis após a captura.
|
||||
# Mantemos referências e copiamos somente no salvamento.
|
||||
self.ultimo_raw_multi = dict(frame)
|
||||
self.timestamp_ultimo_raw_multi = ts_raw_perf
|
||||
self._ultimo_resultado_raw = {
|
||||
"erro": None,
|
||||
"duracao": dur,
|
||||
"frame_valido": True,
|
||||
"cameras": list(frame.keys()),
|
||||
"sync_ok": bool(meta.get("sync_ok", True)),
|
||||
"sync_dt_ms": float(meta.get("sync_dt_ms", 0.0) or 0.0),
|
||||
"frame_id": int(meta.get("frame_id", 0) or 0),
|
||||
"origem": "cache_da_captura_operacional",
|
||||
}
|
||||
|
||||
self.ultimo_frame_rgb = preview_bgr
|
||||
self.timestamp_ultimo_frame_rgb = ts
|
||||
self._ultimo_resultado_rgb = {
|
||||
|
|
@ -679,50 +706,28 @@ class CameraMultispectral:
|
|||
}
|
||||
|
||||
def requisitar_frame_raw_multi(self, force: bool = False, max_age_s: float = None):
|
||||
"""
|
||||
Debug opcional.
|
||||
Retorna dict por câmera:
|
||||
{"CAM_A": raw, "CAM_B": raw, "CAM_C": raw}, resultado
|
||||
|
||||
O weed_worker normal não precisa usar isso.
|
||||
"""
|
||||
"""Retorna cópia do último RAW capturado pelo fluxo operacional."""
|
||||
try:
|
||||
if max_age_s is None or float(max_age_s) <= 0:
|
||||
max_age_s = max(1.0, self._cache_max_age_s)
|
||||
|
||||
agora = time.perf_counter()
|
||||
if max_age_s is None:
|
||||
max_age_s = self._cache_max_age_s
|
||||
|
||||
with self._lock:
|
||||
cache_ok = (
|
||||
self.ultimo_raw_multi is not None
|
||||
and self.timestamp_ultimo_raw_multi is not None
|
||||
and (agora - self.timestamp_ultimo_raw_multi) < max_age_s
|
||||
)
|
||||
if self.ultimo_raw_multi is None or self.timestamp_ultimo_raw_multi is None:
|
||||
raise RuntimeError("cache RAW ainda não disponível")
|
||||
|
||||
if cache_ok and not force:
|
||||
return self.ultimo_raw_multi, dict(self._ultimo_resultado_raw)
|
||||
idade = agora - self.timestamp_ultimo_raw_multi
|
||||
if idade > float(max_age_s):
|
||||
raise RuntimeError(f"cache RAW antigo: {idade:.2f}s")
|
||||
|
||||
if self.client is None:
|
||||
raise RuntimeError("OakFcc3Client não inicializado")
|
||||
|
||||
t0 = time.perf_counter()
|
||||
raw_frame, raw_meta = self.client.get_next_raw_frame(timeout=self.timeout_s)
|
||||
ts = time.perf_counter()
|
||||
dur = ts - t0
|
||||
|
||||
resultado = {
|
||||
"erro": None,
|
||||
"duracao": dur,
|
||||
"frame_valido": bool(raw_frame),
|
||||
"cameras": list(raw_frame.keys()) if isinstance(raw_frame, dict) else [],
|
||||
"sync_ok": bool(raw_meta.get("sync_ok", True)) if isinstance(raw_meta, dict) else True,
|
||||
"sync_dt_ms": float(raw_meta.get("sync_dt_ms", 0.0)) if isinstance(raw_meta, dict) else 0.0,
|
||||
}
|
||||
|
||||
with self._lock:
|
||||
self.ultimo_raw_multi = raw_frame
|
||||
self.ultimo_meta = raw_meta
|
||||
self.timestamp_ultimo_raw_multi = ts
|
||||
self._ultimo_resultado_raw = resultado
|
||||
raw_frame = {
|
||||
cam_id: arr.copy()
|
||||
for cam_id, arr in self.ultimo_raw_multi.items()
|
||||
}
|
||||
resultado = dict(self._ultimo_resultado_raw)
|
||||
resultado["cache_age_s"] = float(idade)
|
||||
resultado["force_ignorado"] = bool(force)
|
||||
|
||||
return raw_frame, resultado
|
||||
|
||||
|
|
@ -732,11 +737,9 @@ class CameraMultispectral:
|
|||
"duracao": 0.0,
|
||||
"frame_valido": False,
|
||||
}
|
||||
|
||||
with self._lock:
|
||||
self._ultimo_resultado_raw = resultado
|
||||
|
||||
self.mostrar_log(f"[CameraMultispectral] Erro ao requisitar RAW multi: {e}")
|
||||
self.mostrar_log(f"[CameraMultispectral] RAW cache indisponível: {e}")
|
||||
return None, resultado
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -744,33 +747,35 @@ class CameraMultispectral:
|
|||
# ============================================================
|
||||
|
||||
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:
|
||||
self.imu.atualizar_saude()
|
||||
|
||||
conectado_ctx = (ContextoGlobalRedis.get_cameras() or {}).get(self.mx_id) is not None
|
||||
|
||||
conectado = bool(self.iniciado and self.client is not None)
|
||||
motivos = []
|
||||
saude = 50
|
||||
status = self._safe_get_status()
|
||||
running = bool(status.get("running", False))
|
||||
conectado = bool(
|
||||
self.iniciado
|
||||
and self.client is not None
|
||||
and running
|
||||
and not self._falha_fatal_pendente
|
||||
)
|
||||
|
||||
performance = {
|
||||
"temperatura": 0.0,
|
||||
"memoria_usada": 0.0,
|
||||
"executando": False,
|
||||
"executando": running,
|
||||
"velocidade": "",
|
||||
"sync_dt_ms": 0.0
|
||||
"sync_dt_ms": 0.0,
|
||||
"async_capture": status.get("async_capture") or {},
|
||||
}
|
||||
|
||||
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))
|
||||
)
|
||||
pipeline_ia_ok = bool(pipeline_ia.get("ok", False))
|
||||
performance["pipeline_ia"] = pipeline_ia
|
||||
|
||||
imu = self.imu.get_dados() if self.imu else {
|
||||
|
|
@ -794,59 +799,55 @@ class CameraMultispectral:
|
|||
resultado = dict(self._ultimo_resultado_tensor)
|
||||
|
||||
try:
|
||||
status = self._safe_get_status()
|
||||
running = bool(status.get("running", False))
|
||||
performance["executando"] = running
|
||||
metrics = (
|
||||
self.client.get_device_metrics()
|
||||
if self.client is not None
|
||||
else {"ok": False, "error": "client indisponível"}
|
||||
)
|
||||
performance["device_metrics"] = metrics
|
||||
|
||||
# Tenta chegar no device real por baixo do service/manager.
|
||||
dev = None
|
||||
try:
|
||||
dev = self.client.svc.manager.device
|
||||
except Exception:
|
||||
dev = None
|
||||
temp = float(metrics.get("temperature_average_c", 0.0) or 0.0)
|
||||
performance["temperatura"] = temp
|
||||
|
||||
if dev is not None:
|
||||
try:
|
||||
temp = float(dev.getChipTemperature().average)
|
||||
performance["temperatura"] = temp
|
||||
if temp >= 80:
|
||||
motivos.append(f"Temperatura crítica: {temp:.1f} °C")
|
||||
saude -= 30
|
||||
elif temp >= 70:
|
||||
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
|
||||
saude -= 15
|
||||
elif temp >= 60:
|
||||
motivos.append(f"Temperatura acima do ideal: {temp:.1f} °C")
|
||||
saude -= 5
|
||||
|
||||
if temp >= 80:
|
||||
motivos.append(f"Temperatura crítica: {temp:.1f} °C")
|
||||
saude -= 30
|
||||
elif temp >= 70:
|
||||
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
|
||||
saude -= 15
|
||||
elif temp >= 60:
|
||||
motivos.append(f"Temperatura acima do ideal: {temp:.1f} °C")
|
||||
saude -= 5
|
||||
except Exception:
|
||||
pass
|
||||
ddr = float(metrics.get("ddr_used_bytes", 0) or 0) / 1024.0 / 1024.0
|
||||
performance["memoria_usada"] = ddr
|
||||
|
||||
try:
|
||||
ddr = float(dev.getDdrMemoryUsage().used) / 1024.0 / 1024.0
|
||||
performance["memoria_usada"] = ddr
|
||||
if ddr >= 500:
|
||||
motivos.append(f"Memória DDR crítica: {ddr:.1f} MB")
|
||||
saude -= 30
|
||||
elif ddr >= 400:
|
||||
motivos.append(f"Memória DDR elevada: {ddr:.1f} MB")
|
||||
saude -= 15
|
||||
elif ddr >= 300:
|
||||
motivos.append(f"Memória DDR acima do ideal: {ddr:.1f} MB")
|
||||
saude -= 5
|
||||
|
||||
if ddr >= 500:
|
||||
motivos.append(f"Memória DDR crítica: {ddr:.1f} MB")
|
||||
saude -= 30
|
||||
elif ddr >= 400:
|
||||
motivos.append(f"Memória DDR elevada: {ddr:.1f} MB")
|
||||
saude -= 15
|
||||
elif ddr >= 300:
|
||||
motivos.append(f"Memória DDR acima do ideal: {ddr:.1f} MB")
|
||||
saude -= 5
|
||||
except Exception:
|
||||
pass
|
||||
speed = str(metrics.get("usb_speed", "") or "")
|
||||
performance["velocidade"] = speed
|
||||
if speed and speed.lower() not in ("super", "superplus"):
|
||||
motivos.append(f"USB lenta: {speed}")
|
||||
saude -= 15
|
||||
|
||||
try:
|
||||
speed = dev.getUsbSpeed().name
|
||||
performance["velocidade"] = speed
|
||||
async_status = metrics.get("async_capture") or performance["async_capture"]
|
||||
performance["async_capture"] = async_status
|
||||
|
||||
if str(speed).lower() not in ["super", "superplus"]:
|
||||
motivos.append(f"USB lenta: {speed}")
|
||||
saude -= 15
|
||||
except Exception:
|
||||
pass
|
||||
if running and async_status:
|
||||
if not bool(async_status.get("thread_alive", False)):
|
||||
motivos.append("Thread assíncrona de captura parada")
|
||||
saude = 0
|
||||
erro_async = async_status.get("last_error")
|
||||
if erro_async:
|
||||
motivos.append(f"Captura assíncrona: {erro_async}")
|
||||
|
||||
if not running:
|
||||
motivos.append("Pipeline parada")
|
||||
|
|
@ -858,19 +859,17 @@ class CameraMultispectral:
|
|||
"duracao": 0.0,
|
||||
"frame_valido": False,
|
||||
}
|
||||
|
||||
if "Communication exception" in str(e) or "X_LINK_ERROR" in str(e):
|
||||
if self._is_erro_fatal_depthai(e):
|
||||
conectado = False
|
||||
|
||||
agora = time.time()
|
||||
ts_tensor = self.timestamp_ultimo_tensor or 0.0
|
||||
frame_recente = (agora - ts_tensor) <= 2.0
|
||||
frame_recente = ts_tensor > 0 and (agora - ts_tensor) <= 2.0
|
||||
|
||||
if resultado.get("sync_dt_ms") is not None:
|
||||
try:
|
||||
performance["sync_dt_ms"] = float(resultado.get("sync_dt_ms", 0.0))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
performance["sync_dt_ms"] = float(resultado.get("sync_dt_ms", 0.0) or 0.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not conectado and not conectado_ctx:
|
||||
motivos.append("desconectado")
|
||||
|
|
@ -901,34 +900,25 @@ 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:
|
||||
if not pipeline_ia_ok:
|
||||
for motivo in pipeline_ia.get("motivos", []) or []:
|
||||
texto = f"Pipeline IA: {motivo}"
|
||||
if texto not in motivos:
|
||||
motivos.append(texto)
|
||||
saude = 0
|
||||
|
||||
motivos = list(dict.fromkeys(str(m) for m in motivos if m))
|
||||
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:
|
||||
elif not frame_recente or not pipeline_ia_ok or saude <= 0:
|
||||
status_mod = StatusModulo.FALHA
|
||||
elif saude < 80:
|
||||
status_mod = StatusModulo.ALERTA
|
||||
|
||||
self.rodando = (
|
||||
conectado
|
||||
and frame_recente
|
||||
and pipeline_ia_ok
|
||||
)
|
||||
self.rodando = bool(conectado and frame_recente and pipeline_ia_ok)
|
||||
|
||||
saude_geral = {
|
||||
"timestamp": agora,
|
||||
|
|
@ -938,7 +928,6 @@ class CameraMultispectral:
|
|||
"motivos": motivos,
|
||||
"saude_individual": [],
|
||||
}
|
||||
|
||||
self.ultima_saude = saude_geral
|
||||
|
||||
from camera_worker.manager import definir_saude_camera
|
||||
|
|
@ -953,7 +942,7 @@ class CameraMultispectral:
|
|||
conectado,
|
||||
agora,
|
||||
self.dispositivo,
|
||||
imu=imu
|
||||
imu=imu,
|
||||
)
|
||||
|
||||
def _definir_heartbeat(self):
|
||||
|
|
@ -1175,129 +1164,54 @@ class CameraMultispectral:
|
|||
|
||||
def requisitar_bundle_raw_multispec(self, force: bool = True, max_age_s: float = None):
|
||||
"""
|
||||
Retorna um bundle científico RAW_BRUTO completo.
|
||||
Monta o bundle a partir do cache do fluxo operacional.
|
||||
|
||||
Retorno:
|
||||
bundle, resultado
|
||||
|
||||
bundle = {
|
||||
"raw_frame": {
|
||||
"CAM_A": np.ndarray RAW10 packed,
|
||||
"CAM_B": np.ndarray RAW10 packed,
|
||||
"CAM_C": np.ndarray RAW10 packed,
|
||||
},
|
||||
"raw_meta": dict,
|
||||
"preview_bgr": np.ndarray BGR uint8 ou None,
|
||||
"preview_method": str,
|
||||
}
|
||||
|
||||
Este método não salva nada em disco.
|
||||
Ele apenas coleta e organiza o pacote bruto.
|
||||
Não chama client.get_next_raw_frame(), portanto não disputa o cursor
|
||||
assíncrono com a inferência. force é mantido por compatibilidade.
|
||||
"""
|
||||
|
||||
try:
|
||||
agora = time.perf_counter()
|
||||
if max_age_s is None or float(max_age_s) <= 0:
|
||||
max_age_s = max(1.0, self._cache_max_age_s)
|
||||
|
||||
if max_age_s is None:
|
||||
max_age_s = self._cache_max_age_s
|
||||
|
||||
with self._lock:
|
||||
cache_ok = (
|
||||
self.ultimo_raw_multi is not None
|
||||
and self.ultimo_meta is not None
|
||||
and self.timestamp_ultimo_raw_multi is not None
|
||||
and (agora - self.timestamp_ultimo_raw_multi) < max_age_s
|
||||
)
|
||||
|
||||
if cache_ok and not force:
|
||||
raw_frame = {
|
||||
cam_id: arr.copy()
|
||||
for cam_id, arr in self.ultimo_raw_multi.items()
|
||||
}
|
||||
|
||||
raw_meta = dict(self.ultimo_meta)
|
||||
|
||||
preview_bgr, preview_method = self._build_preview_raw_multispec(
|
||||
raw_frame=raw_frame,
|
||||
raw_meta=raw_meta,
|
||||
)
|
||||
|
||||
bundle = {
|
||||
"raw_frame": raw_frame,
|
||||
"raw_meta": raw_meta,
|
||||
"preview_bgr": preview_bgr,
|
||||
"preview_method": preview_method,
|
||||
}
|
||||
|
||||
return bundle, dict(self._ultimo_resultado_raw)
|
||||
|
||||
if self.client is None:
|
||||
raise RuntimeError("OakFcc3Client não inicializado")
|
||||
|
||||
t0 = time.perf_counter()
|
||||
|
||||
raw_frame, raw_meta = self.client.get_next_raw_frame(
|
||||
timeout=self.timeout_s
|
||||
raw_frame, resultado = self.requisitar_frame_raw_multi(
|
||||
force=False,
|
||||
max_age_s=max_age_s,
|
||||
)
|
||||
|
||||
dur = time.perf_counter() - t0
|
||||
if raw_frame is None:
|
||||
raise RuntimeError(resultado.get("erro") or "RAW cache indisponível")
|
||||
|
||||
if not isinstance(raw_frame, dict) or not raw_frame:
|
||||
raise RuntimeError(
|
||||
f"RAW_BRUTO inválido. Esperado dict por câmera, veio {type(raw_frame)}"
|
||||
)
|
||||
|
||||
raw_meta = dict(raw_meta or {})
|
||||
|
||||
frame_type = str(raw_meta.get("frame_type", "")).upper()
|
||||
if frame_type and frame_type != "RAW_BRUTO":
|
||||
raise RuntimeError(
|
||||
f"Bundle científico esperado em RAW_BRUTO, mas veio frame_type={frame_type}"
|
||||
)
|
||||
with self._lock:
|
||||
raw_meta = dict(self.ultimo_meta or {})
|
||||
|
||||
required = {"CAM_A", "CAM_B", "CAM_C"}
|
||||
presentes = set(raw_frame.keys())
|
||||
faltando = sorted(required - presentes)
|
||||
|
||||
if faltando:
|
||||
raise RuntimeError(
|
||||
f"RAW_BRUTO incompleto. Faltando câmeras: {faltando}. Presentes: {sorted(presentes)}"
|
||||
f"RAW_BRUTO incompleto. Faltando câmeras: {faltando}. "
|
||||
f"Presentes: {sorted(presentes)}"
|
||||
)
|
||||
|
||||
raw_frame_copy = {
|
||||
cam_id: arr.copy()
|
||||
for cam_id, arr in raw_frame.items()
|
||||
}
|
||||
|
||||
preview_bgr, preview_method = self._build_preview_raw_multispec(
|
||||
raw_frame=raw_frame_copy,
|
||||
raw_frame=raw_frame,
|
||||
raw_meta=raw_meta,
|
||||
)
|
||||
|
||||
resultado = {
|
||||
resultado = dict(resultado)
|
||||
resultado.update({
|
||||
"erro": None,
|
||||
"duracao": dur,
|
||||
"frame_valido": True,
|
||||
"cameras": list(raw_frame_copy.keys()),
|
||||
"sync_ok": bool(raw_meta.get("sync_ok", True)),
|
||||
"sync_dt_ms": float(raw_meta.get("sync_dt_ms", 0.0) or 0.0),
|
||||
"frame_id": raw_meta.get("frame_id"),
|
||||
"preview_method": preview_method,
|
||||
}
|
||||
|
||||
with self._lock:
|
||||
self.ultimo_raw_multi = raw_frame_copy
|
||||
self.ultimo_meta = raw_meta
|
||||
self.timestamp_ultimo_raw_multi = agora
|
||||
self._ultimo_resultado_raw = resultado
|
||||
"origem": "cache_da_captura_operacional",
|
||||
})
|
||||
|
||||
bundle = {
|
||||
"raw_frame": raw_frame_copy,
|
||||
"raw_frame": raw_frame,
|
||||
"raw_meta": raw_meta,
|
||||
"preview_bgr": preview_bgr,
|
||||
"preview_method": preview_method,
|
||||
}
|
||||
|
||||
return bundle, resultado
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -1306,17 +1220,11 @@ class CameraMultispectral:
|
|||
"duracao": 0.0,
|
||||
"frame_valido": False,
|
||||
}
|
||||
|
||||
with self._lock:
|
||||
self._ultimo_resultado_raw = resultado
|
||||
|
||||
self.mostrar_log(
|
||||
f"[CameraMultispectral] Erro ao requisitar bundle RAW multispec: {e}"
|
||||
f"[CameraMultispectral] Erro ao montar bundle RAW cacheado: {e}"
|
||||
)
|
||||
|
||||
if self._is_erro_fatal_depthai(e):
|
||||
self._falha_fatal_depthai(e)
|
||||
|
||||
return None, resultado
|
||||
|
||||
def _build_preview_raw_multispec(self, raw_frame: dict, raw_meta: dict):
|
||||
|
|
|
|||
|
|
@ -173,10 +173,11 @@ class OakFcc3Client:
|
|||
return resp
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
self.svc.stop()
|
||||
finally:
|
||||
self.svc.disconnect()
|
||||
# disconnect() já para o manager. Evita manager.stop() duplicado.
|
||||
return self.svc.disconnect()
|
||||
|
||||
def get_device_metrics(self):
|
||||
return self.svc.get_device_metrics()
|
||||
|
||||
def get_status(self):
|
||||
return self.svc.get_status()
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ class OakFcc3Manager:
|
|||
|
||||
self.running = False
|
||||
self.frame_id = 0
|
||||
|
||||
# Serializa start/stop e leituras nativas de telemetria.
|
||||
# Evita getChipTemperature/getUsbSpeed concorrendo com device.close().
|
||||
self._device_lock = threading.RLock()
|
||||
self.control_queues = {}
|
||||
self.camera_controls = {
|
||||
cam_id: self._default_controls_for_role(role)
|
||||
|
|
@ -746,13 +750,20 @@ class OakFcc3Manager:
|
|||
# ============================================================
|
||||
|
||||
def start(self):
|
||||
with self._device_lock:
|
||||
return self._start_impl()
|
||||
|
||||
def _start_impl(self):
|
||||
if self.running:
|
||||
return
|
||||
|
||||
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.")
|
||||
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."
|
||||
)
|
||||
|
||||
try:
|
||||
self.dev_info = self._resolve_device_info()
|
||||
|
|
@ -781,7 +792,10 @@ class OakFcc3Manager:
|
|||
|
||||
role = self.roles.get(socket_name, "unknown")
|
||||
|
||||
print(f"[OAK] Criando câmera {socket_name} sensor={f.sensorName} role={role}")
|
||||
print(
|
||||
f"[OAK] Criando câmera {socket_name} "
|
||||
f"sensor={f.sensorName} role={role}"
|
||||
)
|
||||
|
||||
cam, output = self._create_camera_node_classic(
|
||||
socket=socket,
|
||||
|
|
@ -813,9 +827,7 @@ class OakFcc3Manager:
|
|||
}
|
||||
|
||||
self._validate_capture_mode()
|
||||
|
||||
self._create_imu_node(self.pipeline)
|
||||
|
||||
self.device.startPipeline(self.pipeline)
|
||||
|
||||
self.q_imu = None
|
||||
|
|
@ -861,21 +873,30 @@ class OakFcc3Manager:
|
|||
self._start_async_capture_thread()
|
||||
time.sleep(0.05)
|
||||
|
||||
if not self.running:
|
||||
erro = self._capture_thread_stats.get("last_error")
|
||||
raise RuntimeError(
|
||||
"Pipeline DepthAI caiu imediatamente após iniciar. "
|
||||
f"Último erro: {erro}"
|
||||
)
|
||||
|
||||
except Exception:
|
||||
# Garante limpeza mesmo se a falha acontecer
|
||||
# no meio da construção do pipeline.
|
||||
# _device_lock é RLock, então a limpeza pode reutilizar stop().
|
||||
try:
|
||||
self.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise
|
||||
|
||||
def stop(self):
|
||||
with self._device_lock:
|
||||
return self._stop_impl()
|
||||
|
||||
def _stop_impl(self):
|
||||
# Impede novas capturas imediatamente.
|
||||
self.running = False
|
||||
|
||||
self._stop_async_capture_thread()
|
||||
thread_ok = self._stop_async_capture_thread()
|
||||
|
||||
try:
|
||||
if self.pipeline is not None and hasattr(self.pipeline, "stop"):
|
||||
|
|
@ -891,6 +912,7 @@ class OakFcc3Manager:
|
|||
|
||||
self.pipeline = None
|
||||
self.device = None
|
||||
self.dev_info = None
|
||||
|
||||
self.queues.clear()
|
||||
self.buffers.clear()
|
||||
|
|
@ -911,15 +933,23 @@ class OakFcc3Manager:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
return bool(thread_ok is not False)
|
||||
|
||||
def _is_fatal_depthai_error(self, erro):
|
||||
txt = str(erro)
|
||||
txt = str(erro).lower()
|
||||
|
||||
sinais = [
|
||||
"X_LINK_ERROR",
|
||||
"Communication exception",
|
||||
"Couldn't read data from stream",
|
||||
"Device already closed",
|
||||
"x_link_error",
|
||||
"communication exception",
|
||||
"couldn't read data from stream",
|
||||
"couldn't open stream",
|
||||
"device already closed",
|
||||
"device has been closed",
|
||||
"x_link_device_not_found",
|
||||
"failed to find device",
|
||||
"no available devices",
|
||||
"nenhum dispositivo depthai",
|
||||
"device crashed",
|
||||
]
|
||||
|
||||
return any(s in txt for s in sinais)
|
||||
|
|
@ -928,29 +958,86 @@ class OakFcc3Manager:
|
|||
# Status
|
||||
# ============================================================
|
||||
|
||||
def get_device_metrics(self):
|
||||
"""Lê métricas nativas sem concorrer com start/stop/device.close()."""
|
||||
with self._device_lock:
|
||||
dev = self.device
|
||||
|
||||
if dev is None or not self.running:
|
||||
return {
|
||||
"ok": False,
|
||||
"running": bool(self.running),
|
||||
"error": "device indisponível",
|
||||
}
|
||||
|
||||
out = {
|
||||
"ok": True,
|
||||
"running": True,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
try:
|
||||
speed = dev.getUsbSpeed()
|
||||
out["usb_speed"] = getattr(speed, "name", str(speed))
|
||||
except Exception as e:
|
||||
out["usb_error"] = str(e)
|
||||
|
||||
try:
|
||||
temp = dev.getChipTemperature()
|
||||
for attr in ("average", "css", "mss", "upa", "dss"):
|
||||
if hasattr(temp, attr):
|
||||
out[f"temperature_{attr}_c"] = float(getattr(temp, attr))
|
||||
except Exception as e:
|
||||
out["temperature_error"] = str(e)
|
||||
|
||||
for nome, metodo in (
|
||||
("ddr", "getDdrMemoryUsage"),
|
||||
("cmx", "getCmxMemoryUsage"),
|
||||
):
|
||||
try:
|
||||
uso = getattr(dev, metodo)()
|
||||
out[f"{nome}_used_bytes"] = int(getattr(uso, "used", 0))
|
||||
out[f"{nome}_total_bytes"] = int(getattr(uso, "total", 0))
|
||||
except Exception as e:
|
||||
out[f"{nome}_error"] = str(e)
|
||||
|
||||
for nome, metodo in (
|
||||
("leon_css", "getLeonCssCpuUsage"),
|
||||
("leon_mss", "getLeonMssCpuUsage"),
|
||||
):
|
||||
try:
|
||||
uso = getattr(dev, metodo)()
|
||||
out[f"{nome}_average"] = float(getattr(uso, "average", uso))
|
||||
except Exception as e:
|
||||
out[f"{nome}_error"] = str(e)
|
||||
|
||||
out["async_capture"] = self.get_async_capture_status()
|
||||
return out
|
||||
|
||||
def get_status(self):
|
||||
return {
|
||||
"mx_id": self.mx_id,
|
||||
"backend": "oak_fcc3",
|
||||
"running": self.running,
|
||||
"fps": self.fps,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"sensor_width": self.sensor_width,
|
||||
"sensor_height": self.sensor_height,
|
||||
"frame_type": self.frame_type,
|
||||
"output_dtype": self.output_dtype,
|
||||
"capture_mode": self.capture_mode,
|
||||
"raw_policy": self.raw_policy,
|
||||
"sync_tolerance_ms": self.sync_tolerance_ms,
|
||||
"buffer_size": self.buffer_size,
|
||||
"geometry_stage": "oak" if self._is_multispec_mode() else "pc",
|
||||
"aligned_geometry": self._serializable_aligned_geometry(),
|
||||
"cameras": list(self.camera_info.values()),
|
||||
"async_capture": self.get_async_capture_status() if hasattr(self, "get_async_capture_status") else None,
|
||||
"tem_imu": bool(getattr(self, "tem_imu", False)),
|
||||
"has_imu_pipeline": bool(getattr(self, "has_imu_pipeline", False)),
|
||||
}
|
||||
with self._device_lock:
|
||||
return {
|
||||
"mx_id": self.mx_id,
|
||||
"backend": "oak_fcc3",
|
||||
"running": bool(self.running),
|
||||
"fps": self.fps,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"sensor_width": self.sensor_width,
|
||||
"sensor_height": self.sensor_height,
|
||||
"frame_type": self.frame_type,
|
||||
"output_dtype": self.output_dtype,
|
||||
"capture_mode": self.capture_mode,
|
||||
"raw_policy": self.raw_policy,
|
||||
"sync_tolerance_ms": self.sync_tolerance_ms,
|
||||
"buffer_size": self.buffer_size,
|
||||
"geometry_stage": "oak" if self._is_multispec_mode() else "pc",
|
||||
"aligned_geometry": self._serializable_aligned_geometry(),
|
||||
"cameras": list(self.camera_info.values()),
|
||||
"async_capture": self.get_async_capture_status(),
|
||||
"tem_imu": bool(getattr(self, "tem_imu", False)),
|
||||
"has_imu_pipeline": bool(getattr(self, "has_imu_pipeline", False)),
|
||||
}
|
||||
|
||||
def _serializable_aligned_geometry(self):
|
||||
if not isinstance(self.aligned_geometry, dict):
|
||||
|
|
|
|||
|
|
@ -13,8 +13,12 @@ class OakFcc3Service:
|
|||
return {"ok": True, "backend": "oak_fcc3", "connected": True}
|
||||
|
||||
def disconnect(self):
|
||||
self.stop()
|
||||
self.connected = False
|
||||
# Único caminho de fechamento do manager.
|
||||
try:
|
||||
self.manager.stop()
|
||||
finally:
|
||||
self.connected = False
|
||||
|
||||
return {"ok": True, "connected": False}
|
||||
|
||||
def ping(self):
|
||||
|
|
@ -33,9 +37,13 @@ class OakFcc3Service:
|
|||
for c in status.get("cameras", [])
|
||||
}
|
||||
|
||||
running = bool(status.get("running", False))
|
||||
realmente_ativo = bool(self.connected and running)
|
||||
|
||||
status.update({
|
||||
"ok": True,
|
||||
"connected": self.connected,
|
||||
"ok": realmente_ativo,
|
||||
"connected": bool(self.connected),
|
||||
"running": running,
|
||||
"active_camera_ids": active_ids,
|
||||
"active_roles": active_roles,
|
||||
"camera_count_active": len(active_ids),
|
||||
|
|
@ -43,6 +51,9 @@ class OakFcc3Service:
|
|||
|
||||
return status
|
||||
|
||||
def get_device_metrics(self):
|
||||
return self.manager.get_device_metrics()
|
||||
|
||||
def get_config(self):
|
||||
return {
|
||||
"mx_id": self.manager.mx_id,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ class CameraManager:
|
|||
self._loop_publicacao_iniciado = False
|
||||
|
||||
self._vida_lock = threading.RLock()
|
||||
self._camera_generation = 0
|
||||
self._falhas_inicializacao = 0
|
||||
self._proxima_inicializacao_monotonic = 0.0
|
||||
self._fechando_camera = False
|
||||
self._em_warmup = False
|
||||
|
||||
|
|
@ -111,6 +114,7 @@ class CameraManager:
|
|||
self._tensor_pronto = None
|
||||
self._tensor_res = None
|
||||
self._tensor_ts = 0.0
|
||||
self._tensor_generation = int(getattr(self, "_camera_generation", 0))
|
||||
self._tensor_consumido_ts = 0.0
|
||||
|
||||
self.pipeline_ia_ok = False
|
||||
|
|
@ -131,6 +135,7 @@ class CameraManager:
|
|||
"infer_prepare_ms": 0.0,
|
||||
"infer_post_ms": 0.0,
|
||||
"fps_model": 0.0,
|
||||
"generation": int(getattr(self, "_camera_generation", 0)),
|
||||
}
|
||||
self._pred_consumido_ts = 0.0
|
||||
|
||||
|
|
@ -196,39 +201,85 @@ class CameraManager:
|
|||
"total_ms": 0.0,
|
||||
}
|
||||
|
||||
def _avancar_geracao_camera(self):
|
||||
self._camera_generation += 1
|
||||
return self._camera_generation
|
||||
|
||||
def _sessao_camera_valida(self, camera, generation):
|
||||
with self._vida_lock:
|
||||
return bool(
|
||||
camera is not None
|
||||
and camera is self.camera
|
||||
and int(generation) == int(self._camera_generation)
|
||||
)
|
||||
|
||||
def _registrar_falha_inicializacao(self):
|
||||
self._falhas_inicializacao += 1
|
||||
atraso = min(10.0, 1.0 * (2 ** min(self._falhas_inicializacao - 1, 3)))
|
||||
self._proxima_inicializacao_monotonic = time.monotonic() + atraso
|
||||
return atraso
|
||||
|
||||
def inicializar(self, mx_id):
|
||||
if mx_id is None:
|
||||
return False
|
||||
|
||||
with self._vida_lock:
|
||||
if self.iniciando:
|
||||
if self.iniciando or self._fechando_camera:
|
||||
return False
|
||||
|
||||
if (
|
||||
self.camera is not None
|
||||
and self.operante
|
||||
and self.pipeline_ia_ok
|
||||
):
|
||||
return True
|
||||
agora_mono = time.monotonic()
|
||||
if agora_mono < self._proxima_inicializacao_monotonic:
|
||||
return False
|
||||
|
||||
camera_existente = self.camera
|
||||
|
||||
if camera_existente is not None:
|
||||
status_existente = camera_existente._safe_get_status()
|
||||
ativa = bool(
|
||||
camera_existente.iniciado
|
||||
and status_existente.get("running", False)
|
||||
)
|
||||
|
||||
if ativa and self.operante and self.pipeline_ia_ok:
|
||||
return True
|
||||
|
||||
# Nunca tenta abrir outra instância sobre uma referência existente.
|
||||
self.fechar_camera_manager(
|
||||
motivo="reinicialização solicitada sobre câmera não saudável",
|
||||
camera_esperada=camera_existente,
|
||||
)
|
||||
# Dá tempo para o runtime/USB liberar a sessão anterior.
|
||||
time.sleep(1.0)
|
||||
|
||||
self.iniciando = True
|
||||
self.pipeline_ia_ok = False
|
||||
camera_nova = None
|
||||
|
||||
try:
|
||||
self.mx_id = mx_id
|
||||
self.mx_id = str(mx_id)
|
||||
|
||||
from weed_worker.config import load_seg_config
|
||||
self.seg_config = load_seg_config()
|
||||
self.posproc_intervalo_min_s = float(
|
||||
self.seg_config.get("posproc_intervalo_min_s", 5.0)
|
||||
)
|
||||
|
||||
self.posproc_intervalo_min_s = float(self.seg_config.get("posproc_intervalo_min_s", 5.0))
|
||||
|
||||
self._inicializar_camera(mx_id, self.seg_config)
|
||||
|
||||
if self.camera is None:
|
||||
self.mostrar_log(f"❌ Camera com ID {mx_id} não iniciada.")
|
||||
camera_nova = self._inicializar_camera(mx_id, self.seg_config)
|
||||
if camera_nova is None:
|
||||
atraso = self._registrar_falha_inicializacao()
|
||||
self.mostrar_log(
|
||||
f"❌ Camera com ID {mx_id} não iniciada. "
|
||||
f"Nova tentativa após backoff de {atraso:.1f}s."
|
||||
)
|
||||
return False
|
||||
|
||||
self.mostrar_log(f"📷 Camera selecionada: {self.camera.modelo} - {self.camera.mx_id}")
|
||||
self.camera = camera_nova
|
||||
generation = self._avancar_geracao_camera()
|
||||
|
||||
self.mostrar_log(
|
||||
f"📷 Camera selecionada: {camera_nova.modelo} - "
|
||||
f"{camera_nova.mx_id} | geração={generation}"
|
||||
)
|
||||
|
||||
self._reset_runtime_state()
|
||||
|
||||
|
|
@ -242,22 +293,34 @@ class CameraManager:
|
|||
self.operante = False
|
||||
self._em_warmup = True
|
||||
|
||||
warmup_ok = self._executar_warmup_camera_manager(n_ciclos=3, timeout_s=12.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] recuperação rejeitada: pipeline não concluiu validação ponta a ponta")
|
||||
self.fechar_camera_manager("warmup incompleto após reconexão")
|
||||
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",
|
||||
camera_esperada=camera_nova,
|
||||
)
|
||||
atraso = self._registrar_falha_inicializacao()
|
||||
self.mostrar_log(f"[weed] backoff de reconexão: {atraso:.1f}s")
|
||||
return False
|
||||
|
||||
if not self._sessao_camera_valida(camera_nova, generation):
|
||||
return False
|
||||
|
||||
self.operante = True
|
||||
|
||||
self._iniciar_loops_se_necessario(self.seg_config)
|
||||
|
||||
# A avaliação final precisa acontecer depois que a inicialização acabou.
|
||||
# Avaliação final precisa ocorrer fora do estado iniciando.
|
||||
self.iniciando = False
|
||||
|
||||
pipeline_ia = self._avaliar_saude_pipeline_ia()
|
||||
|
||||
if not pipeline_ia.get("ok", False):
|
||||
|
|
@ -265,23 +328,44 @@ class CameraManager:
|
|||
"[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)
|
||||
self.fechar_camera_manager(
|
||||
motivo="pipeline IA inválido após warmup",
|
||||
camera_esperada=camera_nova,
|
||||
)
|
||||
atraso = self._registrar_falha_inicializacao()
|
||||
self.mostrar_log(f"[weed] backoff de reconexão: {atraso:.1f}s")
|
||||
return False
|
||||
|
||||
self._falhas_inicializacao = 0
|
||||
self._proxima_inicializacao_monotonic = 0.0
|
||||
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)
|
||||
|
||||
if self.camera is camera_nova and camera_nova is not None:
|
||||
self.fechar_camera_manager(
|
||||
motivo=f"falha de inicialização: {e}",
|
||||
camera_esperada=camera_nova,
|
||||
)
|
||||
elif camera_nova is not None:
|
||||
try:
|
||||
camera_nova.parar()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
atraso = self._registrar_falha_inicializacao()
|
||||
self.mostrar_log(f"[weed] backoff de reconexão: {atraso:.1f}s")
|
||||
return False
|
||||
|
||||
finally:
|
||||
self.iniciando = False
|
||||
self._em_warmup = False
|
||||
|
||||
def _inicializar_camera(self, mx_id, seg_config):
|
||||
nova = None
|
||||
|
||||
try:
|
||||
nova = CameraMultispectral(
|
||||
self.mostrar_log,
|
||||
|
|
@ -289,18 +373,28 @@ class CameraManager:
|
|||
module_calibration_json=seg_config.get("module_calibration_json"),
|
||||
width=int(seg_config.get("camera_width", 1280)),
|
||||
height=int(seg_config.get("camera_height", 800)),
|
||||
fps=int(seg_config.get("camera_fps", 40)),
|
||||
# Default conservador para campo. Se o JSON definir 40, ele será respeitado.
|
||||
fps=int(seg_config.get("camera_fps", 20)),
|
||||
target_size=seg_config.get("ia_resolution", [1024, 640]),
|
||||
)
|
||||
|
||||
if nova.iniciado:
|
||||
self.camera = nova
|
||||
else:
|
||||
self.camera = None
|
||||
if not nova.iniciado:
|
||||
try:
|
||||
nova.parar()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
return nova
|
||||
|
||||
except Exception as e:
|
||||
self.camera = None
|
||||
if nova is not None:
|
||||
try:
|
||||
nova.parar()
|
||||
except Exception:
|
||||
pass
|
||||
self.mostrar_log(f"⚠️ Camera com ID {mx_id} não conectada: {e}")
|
||||
return None
|
||||
|
||||
def _inicializar_modelo(self):
|
||||
if self.model_svc is not None:
|
||||
|
|
@ -408,42 +502,123 @@ class CameraManager:
|
|||
|
||||
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")
|
||||
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
|
||||
|
||||
if self._fechando_camera:
|
||||
return False
|
||||
|
||||
camera_alvo = self.camera
|
||||
geracao_antiga = self._camera_generation
|
||||
|
||||
# Invalida primeiro todos os trabalhos em voo.
|
||||
self._avancar_geracao_camera()
|
||||
self.camera = None
|
||||
self.operante = 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)
|
||||
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}")
|
||||
# Evita reabrir enquanto o dispositivo ainda está reenlistando no USB.
|
||||
self._proxima_inicializacao_monotonic = max(
|
||||
self._proxima_inicializacao_monotonic,
|
||||
time.monotonic() + 1.0,
|
||||
)
|
||||
|
||||
self.mostrar_log(
|
||||
f"[weed] Camera Manager fechado: {motivo} | "
|
||||
f"geração={geracao_antiga}->{self._camera_generation}"
|
||||
)
|
||||
return True
|
||||
|
||||
def reiniciar_deteccoes(self):
|
||||
self._reset_runtime_state()
|
||||
return True, "estado operacional limpo"
|
||||
"""Limpa somente estado da operação, sem tocar na OAK ou no ONNX."""
|
||||
with self._vida_lock:
|
||||
detector = self.weed_detector
|
||||
|
||||
# Descarta resultados pendentes, mas mantém saúde física e timestamps vivos.
|
||||
with self._lock_tensor:
|
||||
self._tensor_consumido_ts = self._tensor_ts
|
||||
|
||||
with self._pred_lock:
|
||||
self._pred_cache = {
|
||||
"ts": 0.0,
|
||||
"tensor_ts": 0.0,
|
||||
"predictions": None,
|
||||
"res": None,
|
||||
"infer_ms": 0.0,
|
||||
"infer_gpu_ms": 0.0,
|
||||
"infer_prepare_ms": 0.0,
|
||||
"infer_post_ms": 0.0,
|
||||
"fps_model": 0.0,
|
||||
"generation": self._camera_generation,
|
||||
}
|
||||
self._pred_consumido_ts = 0.0
|
||||
|
||||
self._ultima_analise = {}
|
||||
self._ultimo_predictions = None
|
||||
self._ultimo_predictions_full = None
|
||||
self._ultimo_controle = None
|
||||
self._ultimo_cmd_bicos = None
|
||||
|
||||
with self._pub_lock:
|
||||
for chave in ("analise", "controle_bicos", "performance_weed", "cmd_controle"):
|
||||
self._pub_cache[chave] = None
|
||||
for chave in (
|
||||
"dirty_analise",
|
||||
"dirty_controle_bicos",
|
||||
"dirty_performance_weed",
|
||||
"dirty_cmd_controle",
|
||||
):
|
||||
self._pub_cache[chave] = False
|
||||
self._pub_cache["ts_analise"] = 0.0
|
||||
|
||||
reset_nome = None
|
||||
if detector is not None:
|
||||
for nome in (
|
||||
"reiniciar_estado_operacional",
|
||||
"reiniciar_deteccoes",
|
||||
"reset_estado_operacional",
|
||||
"reset",
|
||||
):
|
||||
fn = getattr(detector, nome, None)
|
||||
if callable(fn):
|
||||
fn()
|
||||
reset_nome = nome
|
||||
break
|
||||
|
||||
if detector is None or reset_nome is None:
|
||||
# Fallback seguro: recria apenas o detector, nunca câmera/modelo.
|
||||
self.weed_detector = WeedDetector()
|
||||
if self.seg_config and hasattr(self.weed_detector, "atualizar_config"):
|
||||
self.weed_detector.atualizar_config(self.seg_config)
|
||||
reset_nome = "recriado"
|
||||
|
||||
self._forcar_bicos_off(
|
||||
motivo="reinício do estado operacional das detecções",
|
||||
intervalo_min_s=0.0,
|
||||
)
|
||||
|
||||
return True, f"estado operacional limpo | detector={reset_nome}"
|
||||
|
||||
# ============================================================
|
||||
# Warmup
|
||||
|
|
@ -455,6 +630,7 @@ class CameraManager:
|
|||
timeout_s=12.0,
|
||||
):
|
||||
camera_alvo = self.camera
|
||||
generation = self._camera_generation
|
||||
|
||||
if camera_alvo is None:
|
||||
return False
|
||||
|
|
@ -476,7 +652,7 @@ class CameraManager:
|
|||
|
||||
try:
|
||||
while ciclos_ok < n_ciclos and time.monotonic() < deadline:
|
||||
if self.camera is not camera_alvo:
|
||||
if not self._sessao_camera_valida(camera_alvo, generation):
|
||||
self.mostrar_log(
|
||||
"[weed][WARMUP] câmera foi substituída durante o warmup"
|
||||
)
|
||||
|
|
@ -498,6 +674,11 @@ class CameraManager:
|
|||
self.mostrar_log(
|
||||
f"[weed][WARMUP] captura inválida: {erro}"
|
||||
)
|
||||
try:
|
||||
if camera_alvo._is_erro_fatal_depthai(erro):
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
|
|
@ -563,6 +744,10 @@ class CameraManager:
|
|||
analise_completa = self.detectar_ervas(predictions)
|
||||
t_det1 = time.perf_counter()
|
||||
|
||||
if not self._sessao_camera_valida(camera_alvo, generation):
|
||||
self.perf.inc("warmup_descartado_geracao")
|
||||
continue
|
||||
|
||||
if not isinstance(analise_completa, dict):
|
||||
self.mostrar_log(
|
||||
f"[weed][WARMUP] detecção inválida no frame {frame_id}"
|
||||
|
|
@ -581,6 +766,7 @@ class CameraManager:
|
|||
"pred_ts": agora,
|
||||
"infer_full": infer_full,
|
||||
"analise": analise_completa,
|
||||
"generation": generation,
|
||||
}
|
||||
|
||||
self.mostrar_log(
|
||||
|
|
@ -611,6 +797,7 @@ class CameraManager:
|
|||
self._tensor_pronto = ultimo_ciclo["tensor"]
|
||||
self._tensor_res = ultimo_ciclo["res"]
|
||||
self._tensor_ts = ultimo_ciclo["tensor_ts"]
|
||||
self._tensor_generation = ultimo_ciclo["generation"]
|
||||
self._tensor_consumido_ts = ultimo_ciclo["tensor_ts"]
|
||||
|
||||
with self._pred_lock:
|
||||
|
|
@ -624,6 +811,7 @@ class CameraManager:
|
|||
"infer_prepare_ms": 0.0,
|
||||
"infer_post_ms": 0.0,
|
||||
"fps_model": float(self._fps_infer_ema or 0.0),
|
||||
"generation": ultimo_ciclo["generation"],
|
||||
}
|
||||
self._pred_consumido_ts = ultimo_ciclo["pred_ts"]
|
||||
|
||||
|
|
@ -666,15 +854,28 @@ class CameraManager:
|
|||
self._ultima_saude_ts = time.time()
|
||||
|
||||
try:
|
||||
if self.camera is not None:
|
||||
if pipeline_ia is None:
|
||||
pipeline_ia = self._avaliar_saude_pipeline_ia()
|
||||
# Mantém a referência estável durante leitura/publicação da saúde.
|
||||
# fechar_camera_manager aguarda esta seção curta terminar.
|
||||
with self._vida_lock:
|
||||
camera_atual = self.camera
|
||||
|
||||
self.camera.atualizar_saude(pipeline_ia=pipeline_ia)
|
||||
if camera_atual is not None:
|
||||
if pipeline_ia is None:
|
||||
pipeline_ia = self._avaliar_saude_pipeline_ia()
|
||||
camera_atual.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)
|
||||
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,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[saude] erro: {e}")
|
||||
|
|
@ -790,23 +991,28 @@ class CameraManager:
|
|||
tensor5 = self._tensor_pronto
|
||||
res = self._tensor_res
|
||||
ts_tensor = self._tensor_ts
|
||||
generation = self._tensor_generation
|
||||
|
||||
if tensor5 is None or ts_tensor <= 0:
|
||||
return None, None, None
|
||||
return None, None, None, None
|
||||
|
||||
if int(generation) != int(self._camera_generation):
|
||||
return None, None, res, generation
|
||||
|
||||
if ts_tensor == self._tensor_consumido_ts:
|
||||
return None, None, res
|
||||
return None, None, res, generation
|
||||
|
||||
self._tensor_consumido_ts = ts_tensor
|
||||
return tensor5, ts_tensor, res
|
||||
return tensor5, ts_tensor, res, generation
|
||||
|
||||
def _get_prediction_nova_para_deteccao(self):
|
||||
with self._pred_lock:
|
||||
cache = dict(self._pred_cache)
|
||||
|
||||
pred_ts = float(cache.get("ts", 0.0) or 0.0)
|
||||
generation = int(cache.get("generation", -1))
|
||||
|
||||
if pred_ts <= 0:
|
||||
if pred_ts <= 0 or generation != int(self._camera_generation):
|
||||
return None
|
||||
|
||||
if pred_ts == self._pred_consumido_ts:
|
||||
|
|
@ -1122,50 +1328,51 @@ class CameraManager:
|
|||
t0_wall = time.time()
|
||||
t0_perf = time.perf_counter()
|
||||
camera_atual = None
|
||||
generation = -1
|
||||
|
||||
try:
|
||||
if (self.iniciando or self._em_warmup or not self.operante or self.camera is None):
|
||||
if self.iniciando or self._em_warmup or not self.operante or self.camera is None:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
camera_atual = self.camera
|
||||
with self._vida_lock:
|
||||
camera_atual = self.camera
|
||||
generation = self._camera_generation
|
||||
|
||||
tensor5, res = (camera_atual.requisitar_tensor_multispec(force=True))
|
||||
|
||||
erro = (res.get("erro") if isinstance(res, dict) else None)
|
||||
tensor5, res = camera_atual.requisitar_tensor_multispec(force=True)
|
||||
erro = res.get("erro") if isinstance(res, dict) else None
|
||||
|
||||
if erro:
|
||||
reiniciou = self._registrar_falha_captura(camera_atual, erro)
|
||||
|
||||
time.sleep(0.50 if reiniciou else 0.05)
|
||||
continue
|
||||
|
||||
if tensor5 is None:
|
||||
self.perf.inc("tensor_none")
|
||||
|
||||
reiniciou = self._registrar_falha_captura(camera_atual, "tensor None sem erro explícito")
|
||||
|
||||
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.
|
||||
if not self._sessao_camera_valida(camera_atual, generation):
|
||||
self.perf.inc("tensor_descartado_geracao")
|
||||
continue
|
||||
|
||||
self._falhas_captura_consecutivas = 0
|
||||
self._primeira_falha_captura_ts = 0.0
|
||||
|
||||
agora = time.time()
|
||||
perf = (
|
||||
res.get("perf", {})
|
||||
if isinstance(res, dict)
|
||||
else {}
|
||||
)
|
||||
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._tensor_generation = generation
|
||||
|
||||
self._ultimo_tensor_ok_ts = agora
|
||||
|
||||
t1_perf = time.perf_counter()
|
||||
|
||||
self.perf.tick(
|
||||
|
|
@ -1179,25 +1386,23 @@ class CameraManager:
|
|||
preview_ms=float(perf.get("preview_ms", 0.0) or 0.0),
|
||||
frame_ts=agora,
|
||||
idade_frame_ms=0.0,
|
||||
generation=generation,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(f"[weed][CAPTURE] Erro inesperado: {type(e).__name__}: {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:
|
||||
gasto = time.time() - t0_wall
|
||||
restante = periodo - gasto
|
||||
|
||||
if restante > 0:
|
||||
time.sleep(restante)
|
||||
|
||||
|
|
@ -1215,21 +1420,26 @@ class CameraManager:
|
|||
time.sleep(0.02)
|
||||
continue
|
||||
|
||||
if not self.operante or self.camera is None:
|
||||
with self._vida_lock:
|
||||
camera_atual = self.camera
|
||||
|
||||
if not self.operante or camera_atual is None:
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
|
||||
t_get0 = time.perf_counter()
|
||||
tensor5, tensor_ts, res = self._get_tensor_novo_para_inferencia()
|
||||
tensor5, tensor_ts, res, generation = self._get_tensor_novo_para_inferencia()
|
||||
t_get1 = time.perf_counter()
|
||||
|
||||
if tensor5 is None or tensor_ts is None:
|
||||
self.perf.inc("infer_sem_tensor_novo")
|
||||
|
||||
# Espera curta, sem aplicar novamente um período inteiro.
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
|
||||
if not self._sessao_camera_valida(camera_atual, generation):
|
||||
self.perf.inc("infer_descartada_geracao_antes")
|
||||
continue
|
||||
|
||||
t_inf0 = time.perf_counter()
|
||||
predictions = self.model_svc.infer_tensor_fast(
|
||||
tensor5,
|
||||
|
|
@ -1237,17 +1447,13 @@ class CameraManager:
|
|||
)
|
||||
t_inf1 = time.perf_counter()
|
||||
|
||||
infer_ms = (t_inf1 - t_inf0) * 1000.0
|
||||
infer_full = getattr(
|
||||
self.model_svc,
|
||||
"_ultimo_predictions_full",
|
||||
{},
|
||||
) or {}
|
||||
if not self._sessao_camera_valida(camera_atual, generation):
|
||||
self.perf.inc("infer_descartada_geracao_depois")
|
||||
continue
|
||||
|
||||
infer_forward_ms = infer_full.get(
|
||||
"forward_ms",
|
||||
infer_full.get("infer_ms", 0.0),
|
||||
)
|
||||
infer_ms = (t_inf1 - t_inf0) * 1000.0
|
||||
infer_full = getattr(self.model_svc, "_ultimo_predictions_full", {}) or {}
|
||||
infer_forward_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)
|
||||
|
||||
|
|
@ -1273,6 +1479,7 @@ class CameraManager:
|
|||
"infer_prepare_ms": float(infer_prepare_ms or 0.0),
|
||||
"infer_post_ms": float(infer_post_ms or 0.0),
|
||||
"fps_model": float(self._fps_infer_ema or 0.0),
|
||||
"generation": generation,
|
||||
}
|
||||
self._ultima_inferencia_ok_ts = pred_ts
|
||||
|
||||
|
|
@ -1282,7 +1489,6 @@ class CameraManager:
|
|||
self._ultimo_predictions_full = infer_full
|
||||
|
||||
t_loop1 = time.perf_counter()
|
||||
|
||||
self.perf.tick(
|
||||
"inferencia",
|
||||
latencia_ms=(t_loop1 - t_loop0) * 1000.0,
|
||||
|
|
@ -1294,26 +1500,20 @@ class CameraManager:
|
|||
infer_gpu_ms=float(infer_forward_ms or 0.0),
|
||||
tensor_ts=tensor_ts,
|
||||
frame_ts=pred_ts,
|
||||
idade_tensor_ms=(
|
||||
(time.time() - tensor_ts) * 1000.0
|
||||
if tensor_ts else None
|
||||
),
|
||||
idade_tensor_ms=(time.time() - tensor_ts) * 1000.0 if tensor_ts else None,
|
||||
generation=generation,
|
||||
)
|
||||
|
||||
# Só limita depois de uma inferência realmente executada.
|
||||
gasto = time.perf_counter() - t_loop0
|
||||
restante = periodo - gasto
|
||||
|
||||
if restante > 0:
|
||||
time.sleep(restante)
|
||||
|
||||
except Exception as e:
|
||||
self.mostrar_log(
|
||||
f"❌ Erro no loop_inferencia weed: {e}"
|
||||
)
|
||||
self.mostrar_log(f"❌ Erro no loop_inferencia weed: {e}")
|
||||
time.sleep(0.02)
|
||||
|
||||
threading.Thread(target=loop, daemon=True).start()
|
||||
threading.Thread(target=loop, name="WeedInference", daemon=True).start()
|
||||
|
||||
def _iniciar_loop_deteccao_weed(self, freq=25.0):
|
||||
def loop():
|
||||
|
|
@ -1351,6 +1551,14 @@ class CameraManager:
|
|||
predictions = pred_cache.get("predictions")
|
||||
pred_ts = float(pred_cache.get("ts", 0.0) or 0.0)
|
||||
tensor_ts = float(pred_cache.get("tensor_ts", 0.0) or 0.0)
|
||||
generation = int(pred_cache.get("generation", -1))
|
||||
|
||||
with self._vida_lock:
|
||||
camera_atual = self.camera
|
||||
|
||||
if not self._sessao_camera_valida(camera_atual, generation):
|
||||
self.perf.inc("det_descartada_geracao_antes")
|
||||
continue
|
||||
|
||||
if predictions is None:
|
||||
self.perf.inc("det_predictions_none")
|
||||
|
|
@ -1373,65 +1581,59 @@ class CameraManager:
|
|||
analise_convertida = converter_valores_numpy(analise)
|
||||
t_conv1 = time.perf_counter()
|
||||
|
||||
t_pub_analise0 = time.perf_counter()
|
||||
self._set_pub_cache("analise", {
|
||||
"ts_analise": pred_ts,
|
||||
"fps_model": pred_cache.get("fps_model", 0.0),
|
||||
"fps_inferencia": self._fps_infer_ema,
|
||||
"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()
|
||||
|
||||
# ====================================================
|
||||
# Gate de pulverização
|
||||
# ====================================================
|
||||
# O detector pode enxergar erva, mas só liberamos bicos se:
|
||||
# - pulverizador automático ligado;
|
||||
# - operação EmAndamento;
|
||||
# - não finalizando/pausado/emergência/calibrando;
|
||||
# - trajetória/status do carro = CaminhandoRua;
|
||||
# - há comando/velocidade de movimento.
|
||||
#
|
||||
# Se qualquer item falhar, TODOS os bicos são OFF.
|
||||
# Isso corrige o caso: operação ainda EmAndamento, mas Trajetoria.status=Parado.
|
||||
|
||||
t_ctrl0 = time.perf_counter()
|
||||
|
||||
controle_detectado = analise.get("controle") or {}
|
||||
|
||||
atuacao_bicos, debug_pulverizacao = self._aplicar_gate_pulverizacao(
|
||||
controle_detectado
|
||||
)
|
||||
|
||||
analise["controle"] = atuacao_bicos
|
||||
analise["pulverizacao"] = debug_pulverizacao
|
||||
|
||||
self._ultimo_controle = atuacao_bicos
|
||||
self._ultimo_cmd_bicos = atuacao_bicos
|
||||
t_ctrl1 = time.perf_counter()
|
||||
|
||||
t_pub_ctrl0 = time.perf_counter()
|
||||
self._set_pub_cache("controle_bicos", atuacao_bicos)
|
||||
t_pub_ctrl1 = time.perf_counter()
|
||||
# Publicação atômica em relação a fechar/substituir câmera.
|
||||
# Se a geração mudou, nenhum dado antigo chega aos caches/bicos.
|
||||
with self._vida_lock:
|
||||
if not self._sessao_camera_valida(camera_atual, generation):
|
||||
self.perf.inc("det_descartada_geracao_depois")
|
||||
continue
|
||||
|
||||
t_pub_cmd0 = time.perf_counter()
|
||||
self._set_pub_cache("cmd_controle", {
|
||||
"cmd": WeedWorkerCommandType.EnviarDadosControle.value,
|
||||
"params": atuacao_bicos,
|
||||
"ts": time.time(),
|
||||
"origem": "weed_worker",
|
||||
"pulverizacao_permitida": bool(debug_pulverizacao.get("permitida", False)),
|
||||
"motivo": debug_pulverizacao.get("motivo", ""),
|
||||
"forcado_off": not bool(debug_pulverizacao.get("permitida", False)),
|
||||
})
|
||||
t_pub_cmd1 = time.perf_counter()
|
||||
t_pub_analise0 = time.perf_counter()
|
||||
self._set_pub_cache("analise", {
|
||||
"ts_analise": pred_ts,
|
||||
"fps_model": pred_cache.get("fps_model", 0.0),
|
||||
"fps_inferencia": self._fps_infer_ema,
|
||||
"infer_ms": pred_cache.get("infer_ms", 0.0),
|
||||
"infer_gpu_ms": pred_cache.get("infer_gpu_ms", 0.0),
|
||||
"analise": analise_convertida,
|
||||
"generation": generation,
|
||||
}, ts_origem=pred_ts)
|
||||
t_pub_analise1 = time.perf_counter()
|
||||
|
||||
self._ultima_analise = analise_completa.copy()
|
||||
self._ultimo_controle = atuacao_bicos
|
||||
self._ultimo_cmd_bicos = atuacao_bicos
|
||||
|
||||
# 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_pub_ctrl0 = time.perf_counter()
|
||||
self._set_pub_cache("controle_bicos", atuacao_bicos)
|
||||
t_pub_ctrl1 = time.perf_counter()
|
||||
|
||||
t_pub_cmd0 = time.perf_counter()
|
||||
self._set_pub_cache("cmd_controle", {
|
||||
"cmd": WeedWorkerCommandType.EnviarDadosControle.value,
|
||||
"params": atuacao_bicos,
|
||||
"ts": time.time(),
|
||||
"origem": "weed_worker",
|
||||
"pulverizacao_permitida": bool(debug_pulverizacao.get("permitida", False)),
|
||||
"motivo": debug_pulverizacao.get("motivo", ""),
|
||||
"forcado_off": not bool(debug_pulverizacao.get("permitida", False)),
|
||||
"generation": generation,
|
||||
})
|
||||
t_pub_cmd1 = time.perf_counter()
|
||||
|
||||
self._ultima_analise = analise_completa.copy()
|
||||
self._ultima_deteccao_ok_ts = time.time()
|
||||
|
||||
t_loop1 = time.perf_counter()
|
||||
total_ms = (t_loop1 - t_loop0) * 1000.0
|
||||
|
|
@ -1458,6 +1660,7 @@ class CameraManager:
|
|||
"pred_age_ms": (time.time() - pred_ts) * 1000.0 if pred_ts else None,
|
||||
"tensor_age_ms": (time.time() - tensor_ts) * 1000.0 if tensor_ts else None,
|
||||
"pulverizacao": debug_pulverizacao,
|
||||
"generation": generation,
|
||||
}
|
||||
|
||||
pub_cache_ms = (
|
||||
|
|
@ -2327,13 +2530,19 @@ class CameraManager:
|
|||
)
|
||||
continue
|
||||
|
||||
def salvar_pp_async(nome_frame_async, pasta_async):
|
||||
with self._vida_lock:
|
||||
camera_save = self.camera
|
||||
generation_save = self._camera_generation
|
||||
|
||||
def salvar_pp_async(nome_frame_async, pasta_async, camera_esperada, generation_esperada):
|
||||
try:
|
||||
t_pp0 = time.perf_counter()
|
||||
|
||||
caminhos = self._salvar_bruto_multiespectral(
|
||||
nome_base=nome_frame_async,
|
||||
pasta=pasta_async
|
||||
pasta=pasta_async,
|
||||
camera_esperada=camera_esperada,
|
||||
generation_esperada=generation_esperada,
|
||||
)
|
||||
|
||||
t_pp1 = time.perf_counter()
|
||||
|
|
@ -2355,7 +2564,7 @@ class CameraManager:
|
|||
|
||||
threading.Thread(
|
||||
target=salvar_pp_async,
|
||||
args=(nome_frame, pasta),
|
||||
args=(nome_frame, pasta, camera_save, generation_save),
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
|
|
@ -2438,23 +2647,44 @@ class CameraManager:
|
|||
except Exception:
|
||||
return frame
|
||||
|
||||
def _salvar_bruto_multiespectral(self, nome_base: str, pasta: str):
|
||||
def _salvar_bruto_multiespectral(
|
||||
self,
|
||||
nome_base: str,
|
||||
pasta: str,
|
||||
camera_esperada=None,
|
||||
generation_esperada=None,
|
||||
):
|
||||
try:
|
||||
if not hasattr(self, "camera") or self.camera is None:
|
||||
with self._vida_lock:
|
||||
camera_atual = self.camera
|
||||
generation_atual = self._camera_generation
|
||||
|
||||
if camera_esperada is not None and camera_atual is not camera_esperada:
|
||||
self.mostrar_log("[weed][SAVE_PP] cancelado: câmera foi substituída")
|
||||
return []
|
||||
|
||||
if generation_esperada is not None and int(generation_atual) != int(generation_esperada):
|
||||
self.mostrar_log("[weed][SAVE_PP] cancelado: geração da câmera mudou")
|
||||
return []
|
||||
|
||||
if camera_atual is None:
|
||||
self.mostrar_log("⚠️ CameraMultispectral indisponível para salvar RAW multispec.")
|
||||
return []
|
||||
|
||||
if not hasattr(self.camera, "salvar_bundle_raw_multispec"):
|
||||
self.mostrar_log("⚠️ CameraMultispectral ainda não possui salvar_bundle_raw_multispec().")
|
||||
if not hasattr(camera_atual, "salvar_bundle_raw_multispec"):
|
||||
self.mostrar_log(
|
||||
"⚠️ CameraMultispectral ainda não possui salvar_bundle_raw_multispec()."
|
||||
)
|
||||
return []
|
||||
|
||||
return self.camera.salvar_bundle_raw_multispec(
|
||||
return camera_atual.salvar_bundle_raw_multispec(
|
||||
pasta=pasta,
|
||||
nome=nome_base,
|
||||
nota="pos_processamento_operacao",
|
||||
extra_meta={
|
||||
"origem_chamada": "weed_worker.salvar_frames",
|
||||
"tipo_requisitado": "PosProcessamento",
|
||||
"camera_generation": generation_atual,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue