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.

150 lines
5.8 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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