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