from __future__ import annotations import asyncio, logging, os, threading # UVLoop (optional) try: import uvloop # type: ignore asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) except Exception: pass # Logging setup def setup_logging() -> logging.Logger: os.makedirs("logs", exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler("logs/ptz_tracker.log", encoding="utf-8"), logging.StreamHandler(), ], ) return logging.getLogger("PTZTracker") logger = setup_logging() from . import config, state from .streaming import video_broadcaster, cpp_ws_loop, cpp_detection_loop, MicroBatcher, build_servers from .events import alarm_sender # WS-отправитель тревог (минимальный payload) from .alarms import alarm_http_sender # HTTP-отправитель тревог (маршрут с MAC) from .status import ptz_status_poller from .heartbeat import heartbeat_pinger from .sector import sector_autocal_from_presets, sector_init_on_startup from .calibration import calibrate_presets, autocal_pan_sign, park_to_patrol_start from .patrol import patrol_init_on_startup, patrol_supervisor_tick_bounded, patrol_supervisor_tick, sync_patrol_coordinator_loop from . import ptz_io async def main() -> None: state.MAIN_LOOP = asyncio.get_running_loop() state.alarm_queue = asyncio.Queue() state.detection_queue = asyncio.Queue() state.batcher = MicroBatcher(max_batch=config.BATCH_MIN, max_wait_ms=2.0, adaptive=config.ADAPTIVE_BATCH_ENABLE, bmin=config.BATCH_MIN, bmax=config.BATCH_MAX, target_ms=config.BATCH_TARGET_MS) # Start PTZ worker thread from queue import Queue state.ptz_cmd_q = Queue(maxsize=2048) state._ptz_thread = threading.Thread(target=ptz_io._ptz_worker, name="PTZWorker", daemon=True) state._ptz_thread.start() # Calibrations for cam_id in config.PTZ_CAM_IDS: cfg = config.CAMERA_CONFIG[cam_id] need = ( cfg.get("preset1_deg") is None or cfg.get("preset2_deg") is None or cfg.get("sector_min_deg") is None or cfg.get("sector_max_deg") is None ) if need: ok = await calibrate_presets(cam_id) if not ok: logger.warning("Failed to calibrate camera %s, using fallback for sector clamp.", cam_id) if config.USE_PRESET_EDGES_FOR_SECTOR: need_sector = any(state.ptz_states[c].get("sector_left_deg") is None for c in config.PTZ_CAM_IDS) if need_sector: await sector_autocal_from_presets() sector_init_on_startup() for cam in config.PTZ_CAM_IDS: try: await autocal_pan_sign(cam) except Exception as e: logger.debug("autocal pan_sign cam %s: %s", cam, e) await park_to_patrol_start() if config.PTZ_ORDER and config.PATROL_ENABLE_ON_START: if config.SYNC_PATROL_ENABLE and config.SYNC_COHORTS: logger.info("SYNC PTZ patrol enabled (cohorts): %s", ", ".join(config.SYNC_COHORTS.keys())) else: patrol_init_on_startup() logger.info("PTZ patrol started (standalone)") else: logger.info("PTZ patrol is DISABLED on start") logger.info("PTZ tracking + HARD sector clamp + bounded patrol (resync enabled)") ctrl_server, video_server = await build_servers() logger.info("[CTRL WS] listening on 0.0.0.0:%s", config.VUE_CONTROL_WS_PORT) logger.info("[VIDEO WS] listening on 0.0.0.0:%s", config.VUE_VIDEO_WS_PORT) async def patrol_supervisor_loop(): while True: try: patrol_supervisor_tick_bounded() patrol_supervisor_tick() except Exception as e: logger.error("patrol_supervisor error: %s", e) await asyncio.sleep(0.10) tasks: list[asyncio.Task | asyncio.Future] = [ asyncio.create_task(video_broadcaster()), asyncio.create_task(cpp_ws_loop()), asyncio.create_task(cpp_detection_loop()), asyncio.create_task(alarm_sender()), # WS тревоги ({"type":"freq_alarm","data":...}) asyncio.create_task(alarm_http_sender()), # HTTP тревоги (включается через config.ALARM_HTTP_ENABLE) asyncio.create_task(ptz_status_poller()), asyncio.create_task(heartbeat_pinger()), asyncio.create_task(ctrl_server.wait_closed()), asyncio.create_task(video_server.wait_closed()), ] if config.PATROL_ENABLE_ON_START: tasks.insert(6, asyncio.create_task(patrol_supervisor_loop())) if config.SYNC_PATROL_ENABLE and config.SYNC_COHORTS: tasks.insert(6, asyncio.create_task(sync_patrol_coordinator_loop())) try: await asyncio.gather(*tasks) finally: ctrl_server.close() video_server.close() await ctrl_server.wait_closed() await video_server.wait_closed() def run(): try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Stopped by user") finally: try: if state.ptz_cmd_q is not None: state.ptz_cmd_q.put_nowait(None) # sentinel for PTZ worker if state._ptz_thread is not None: state._ptz_thread.join(timeout=0.5) except Exception: pass for _cid, _st in state.ptz_states.items(): if _st.get("zoom_reset_timer"): try: _st["zoom_reset_timer"].cancel() except Exception: pass if _st.get("preset_timer"): try: _st["preset_timer"].cancel() except Exception: pass logger.info("Shutdown complete") if __name__ == "__main__": run()