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
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
|