from __future__ import annotations import time from collections import defaultdict, deque from typing import Any, Dict, Optional, Tuple, List, Set try: from . import config # нормальный пакетный импорт except Exception: # если файл импортнули как топ-левел import config as config # type: ignore # ---------------- Clients / WS ---------------- vue_control_clients: Set[Any] = set() video_clients: Set[Any] = set() detected_cameras: Set[int] = set() # Порядок и маппинг «плиток» (обновляется из streaming._broadcast_init_to_vue) last_broadcast_ids: List[int] = [] index_to_cam_id: Dict[int, int] = {} # 1-based: 1 -> cam_id_1, 2 -> cam_id_2, ... # ---------------- Queues/loop (init in main) ---------------- alarm_queue: Optional[Any] = None detection_queue: Optional[Any] = None MAIN_LOOP: Optional[Any] = None # ---------------- PTZ thread + queue (init in main) ---------------- ptz_cmd_q: Optional[Any] = None _ptz_thread: Optional[Any] = None # ---------------- MicroBatcher (init in main) ---------------- batcher: Optional[Any] = None # ---------------- Preview frames ---------------- latest_jpeg_by_cam: Dict[int, Tuple[int, bytes]] = {} frame_id_by_cam: Dict[int, int] = {} # ---------------- Detect stats ---------------- detect_stats_global = {"last_ts": None, "ema_interval": None, "times": deque(maxlen=512)} detect_stats_cam = defaultdict(lambda: {"last_ts": None, "ema_interval": None, "times": deque(maxlen=256)}) # ---------------- Sync runtime (cohorts) ---------------- sync_state: Dict[str, Dict[str, Any]] = { g: {"dir": +1, "leg_idx": 0, "leg_start": 0.0, "leg_end": 0.0, "next_start_at": 0.0} for g in config.SYNC_COHORTS } # ---------------- Default PTZ state factory ---------------- def _default_ptz_state(cam_id: int) -> Dict[str, Any]: """ Полный набор ключей, чтобы все модули (postprocess/patrol/ptz_io/…) не ловили KeyError при «горячем» добавлении камеры. """ now = time.time() is_ptz = bool(cam_id in config.PTZ_CAM_IDS) return { # Tracking / kinematics "last_seen": now, "returning": False, "last_dx": 0.0, "last_dy": 0.0, "ix": 0.0, "iy": 0.0, "vx": 0.0, "vy": 0.0, "prev_t": None, "prev_cx": None, "prev_cy": None, "prev_w_frac": None, "w_growth": 0.0, "w_frac_ema": None, "mode": "IDLE", # Image history "prev_gray": None, "of_pts": None, "last_bbox": None, # Zoom/aim dynamics "zoom_state": 0, "zoom_int": 0.0, "zoom_prev_cmd": 0.0, "zoom_ramp_start": 0.0, "zoom_last_change": 0.0, "zoom_lock": False, "lock_candidate_since": None, "was_zoomed_in": False, "zoom_reset_timer": None, # asyncio.TimerHandle | None # Preset timers "preset_timer": None, # asyncio.TimerHandle | None "preset_target": None, # str | int | None # Command/throttle bookkeeping "proc_busy": False, "last_focus_time": 0.0, "last_ptz_send": 0.0, "last_cmd": (0.0, 0.0, 0.0), # (pan, tilt, zoom) -1..+1 "last_cmd_ts": 0.0, "last_pan": 0.0, "last_tilt": 0.0, "last_zoom": 0.0, "ptz_busy_until": 0.0, "pan_sign": 1, # Modes / loss recovery "mode": "IDLE", "loss_since": None, "recover_until": 0.0, "fallback_done": False, "center_lock": 0, "scan_phase": 0, "next_scan_at": 0.0, "zoom_hold_until": 0.0, "az_none_since": None, "last_return_at": 0.0, # Stats / UI "track_history": deque(maxlen=12), "stable_frames": 0, "alarm_sent": False, "miss_frames": 0, # Patrol "patrol_dir": +1, "patrol_active": False, "patrol_idx": 0, # <- пригодится управлялке патруля "patrol_pause_until": 0.0, # <- мягкие паузы между ногами "leg_start": 0.0, "leg_end": 0.0, "endpoint_latch": 0, # Sweep "sweep_active": False, "sweep_pan": 0.0, "sweep_start_at": 0.0, # Anti-smear / shots "shot_next_at": 0.0, "shot_pause_until": 0.0, # Absolute commands "last_abs_send": 0.0, "last_abs_deg": None, # PTZ status (telemetry) # общие поля, которые раньше были: "az_deg": None, # относительный пан от драйвера (если такой есть) "tilt_deg": None, # относительный тилт от драйвера (если такой есть) # добавленные поля для абсолютной геометрии/оптики: "pan_deg_abs": None, # абсолютный пан 0..360, привязанный к северу "tilt_deg_abs": None, # абсолютный наклон (если доступен) "hfov_deg": None, # точный HFOV PTZ на текущем зуме (если знаем) "abs_zoom": 0.0, # абсолютное значение зума (масштаб зависит от твоего статус-пуллера) # Sector (soft bounds) "sector_left_deg": None, "sector_right_deg": None, "sector_center_deg": None, # Sync mode "sync_wait": False, # Recording debounce (CPP CTRL channel) "rec_active": False, "rec_first_seen": None, "rec_started_at": 0.0, "rec_last_seen": 0.0, "rec_last_off_at": 0.0, "rec_last_sent": None, # Meta "is_ptz": is_ptz, } # ---------------- Runtime PTZ state map ---------------- ptz_states: Dict[int, Dict[str, Any]] = {} # Инициализация для камер, которые уже были в конфиге на момент импорта _now = time.time() for _cid in config.CAMERA_CONFIG: # Берём дефолты и при необходимости можно дополнять из существующего (если будет перезагрузка модуля) ptz_states[_cid] = _default_ptz_state(_cid) # ---------------- Public helpers ---------------- def ensure_cam_state(cam_id: int, *, is_ptz: Optional[bool] = None) -> Dict[str, Any]: """ Гарантирует наличие и полноту структуры состояния для cam_id. Если камера уже есть — дольём недостающие ключи без перетирания. is_ptz можно подсказать явно; если None — вычислим по config.PTZ_CAM_IDS. """ st = ptz_states.get(cam_id) if st is None: st = _default_ptz_state(cam_id) if is_ptz is not None: st["is_ptz"] = bool(is_ptz) ptz_states[cam_id] = st return st # Дополним недостающие ключи (на случай старого состояния) defaults = _default_ptz_state(cam_id) for k, v in defaults.items(): if k not in st: st[k] = v # Актуализируем флаг is_ptz, если указан явно if is_ptz is not None: st["is_ptz"] = bool(is_ptz) else: st["is_ptz"] = bool(cam_id in config.PTZ_CAM_IDS) return st