from __future__ import annotations import time from typing import Optional from . import state, config from .events import _send_record_event from .ptz_io import stop_ptz # <-- важно для "abort goto" def _ema_update(prev: Optional[float], value: float, alpha: float = 0.2) -> float: return value if prev is None else (1.0 - alpha) * prev + alpha * value def _stat_on_detection(cam_idx: int, ts: float) -> None: g = state.detect_stats_global if g["last_ts"] is not None: dt = ts - g["last_ts"] if dt > 0.05: g["ema_interval"] = _ema_update(g["ema_interval"], dt) g["times"].append(ts) g["last_ts"] = ts c = state.detect_stats_cam[cam_idx] if c["last_ts"] is not None: dtc = ts - c["last_ts"] if dtc > 0.05: c["ema_interval"] = _ema_update(c["ema_interval"], dtc) c["times"].append(ts) c["last_ts"] = ts def notify_detected(cam_idx: int, detected: bool, force: bool = False) -> None: """ detected=True/False — «сырое» состояние текущего кадра. Формирует START/STOP записи (с гистерезисом) ТОЛЬКО для PTZ (или force). Также ведёт режимы TRACK/SEARCH с hold на потерю, чтобы не срывать трекинг при прерывистой детекции. """ # Панорамы не создают событий записи, если не включено явно if cam_idx not in config.PTZ_CAM_IDS and not config.ALLOW_PANORAMA_RECORD and not force: return st = state.ptz_states[cam_idx] now = time.time() # -------------------- Параметры поведения трекинга -------------------- TRACK_LOSS_HOLD_SEC = float(getattr(config, "TRACK_LOSS_HOLD_SEC", 2.0)) TRACK_HOLD_WHILE_REC_ACTIVE_SEC = float( getattr(config, "TRACK_HOLD_WHILE_REC_ACTIVE_SEC", TRACK_LOSS_HOLD_SEC) ) # -------------------- Внутренняя логика трекинга -------------------- last_raw = st.get("last_detected_state") if detected: # статистика по фронту False->True if last_raw is not True: _stat_on_detection(cam_idx, now) # фиксируем "видели цель" st["last_seen"] = now # Если в этот момент камера ехала на пресет (goto) — прерываем, # иначе goto/патруль будет мешать трекингу первые секунды if now < float(st.get("goto_in_progress_until", 0.0) or 0.0): try: stop_ptz(cam_idx) except Exception: pass st["goto_in_progress_until"] = 0.0 # сброс кандидата на потерю st.pop("track_loss_since", None) # ВАЖНО: сообщаем патрулю, что идёт трекинг st["tracking_active"] = True # трекинг включается сразу st["fallback_done"] = False st["mode"] = "TRACK" # опционально: если используешь где-то пост-трекинг флаги st.pop("post_track_since", None) st["post_track_park_done"] = False else: # detected == False if st.get("mode") == "TRACK": if "track_loss_since" not in st: st["track_loss_since"] = now else: hold = TRACK_HOLD_WHILE_REC_ACTIVE_SEC if bool(st.get("rec_active", False)) else TRACK_LOSS_HOLD_SEC if (now - float(st["track_loss_since"])) >= hold: st.pop("track_loss_since", None) st["mode"] = "SEARCH" st["tracking_active"] = False st["post_track_since"] = now else: # не трекаем — на всякий случай st["tracking_active"] = False st["last_detected_state"] = detected # -------------------- форс-STOP -------------------- if force: if st.get("rec_active", False) or st.get("rec_last_sent") is not False: _send_record_event(cam_idx, False) st["rec_active"] = False st["rec_first_seen"] = None st["rec_started_at"] = 0.0 st["rec_last_off_at"] = now st["rec_last_sent"] = False # сброс трекинга st.pop("track_loss_since", None) st["tracking_active"] = False if st.get("mode") == "TRACK": st["mode"] = "SEARCH" return # -------------------- гистерезис записи -------------------- if detected: st["rec_last_seen"] = now if not st.get("rec_active", False): if now - float(st.get("rec_last_off_at", 0.0) or 0.0) < float(getattr(config, "REC_MIN_OFF_SEC", 0.0)): return if st.get("rec_first_seen") is None: st["rec_first_seen"] = now if (now - float(st["rec_first_seen"])) >= float(getattr(config, "REC_ON_CONFIRM_SEC", 0.35)): st["rec_active"] = True st["rec_started_at"] = now if st.get("rec_last_sent") is not True: _send_record_event(cam_idx, True) # START → C++ st["rec_last_sent"] = True return # detected == False st["rec_first_seen"] = None if st.get("rec_active", False): rec_last_seen = float(st.get("rec_last_seen", 0.0) or 0.0) rec_started_at = float(st.get("rec_started_at", 0.0) or 0.0) long_no_see = (now - rec_last_seen) >= float(getattr(config, "REC_OFF_GRACE_SEC", 1.2)) long_enough = (now - rec_started_at) >= float(getattr(config, "REC_MIN_ON_SEC", 0.0)) too_long = (now - rec_started_at) >= float(getattr(config, "REC_MAX_ON_SEC", 1e9)) if (long_no_see and long_enough) or too_long: st["rec_active"] = False st["rec_last_off_at"] = now if st.get("rec_last_sent") is not False: _send_record_event(cam_idx, False) # STOP → C++ st["rec_last_sent"] = False