You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

202 lines
7.5 KiB
Python

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