Files
VisDrone-Version/data_pusher.py
2025-08-05 16:55:45 +08:00

249 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import base64
import cv2
import numpy as np
import requests
import time
import datetime
import logging
from fastapi import APIRouter, HTTPException, Body # 切换到 APIRouter
from pydantic import BaseModel, HttpUrl
# import uvicorn # 不再由此文件运行uvicorn
from apscheduler.schedulers.background import BackgroundScheduler
from typing import Optional # 用于类型提示
# 确保 RFDETRDetector 可以被导入,假设 rfdetr_core.py 在同一目录或 PYTHONPATH 中
# from rfdetr_core import RFDETRDetector # 在实际使用中取消注释并确保路径正确
# 配置日志记录
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# app = FastAPI(title="Data Pusher Service", version="1.0.0") # 移除独立的 FastAPI app
pusher_router = APIRouter() # 创建一个 APIRouter
class DataPusher:
def __init__(self, detector): # detector: RFDETRDetector
if detector is None:
logger.error("DataPusher initialized with a None detector. Push functionality will be impaired.")
# 仍然创建实例但功能会受限_get_data_payload 会处理 detector is None
self.detector = detector
self.scheduler = BackgroundScheduler(daemon=True)
self.push_job_id = "rt_push_job"
self.target_url = None
if not self.scheduler.running:
try:
self.scheduler.start()
except Exception as e:
logger.error(f"Error starting APScheduler in DataPusher: {e}")
def update_detector_instance(self, detector):
"""允许在运行时更新检测器实例,例如当主应用切换模型时"""
logger.info(f"DataPusher's detector instance is being updated.")
self.detector = detector
if detector is None:
logger.warning("DataPusher's detector instance updated to None.")
def _get_data_payload(self):
"""获取当前的类别计数和最新标注的帧"""
if self.detector is None:
logger.warning("DataPusher: Detector not available. Cannot get data payload.")
return { # 即使检测器不可用,也返回一个结构,包含空数据
# "timestamp": time.time(),
"category_counts": {},
"frame_base64": None,
"error": "Detector not available"
}
category_counts = getattr(self.detector, 'category_counts', {})
# 如果 detector 存在但没有 last_annotated_frame (例如模型刚加载还没处理第一帧)
last_frame_np = getattr(self.detector, 'last_annotated_frame', None)
frame_base64 = None
if last_frame_np is not None and isinstance(last_frame_np, np.ndarray):
try:
_, buffer = cv2.imencode('.jpg', last_frame_np)
frame_base64 = base64.b64encode(buffer).decode('utf-8')
except Exception as e:
logger.error(f"Error encoding frame to base64: {e}")
return {
# "timestamp": time.time(),
"category_counts": category_counts,
"frame_base64": frame_base64
}
def _push_data_task(self):
"""执行数据推送的任务"""
if not self.target_url:
# logger.warning("Target URL not set. Skipping push task.") # 减少日志噪音,仅在初次设置时记录
return
payload = self._get_data_payload()
# if payload is None: # _get_data_payload 现在总会返回一个字典
# logger.warning("No payload to push.")
# return
try:
response = requests.post(self.target_url, json=payload, timeout=5)
response.raise_for_status()
logger.debug(f"Data pushed successfully to {self.target_url}. Status: {response.status_code}") # 改为 debug 级别
except requests.exceptions.RequestException as e:
logger.error(f"Error pushing data to {self.target_url}: {e}")
except Exception as e:
logger.error(f"An unexpected error occurred during data push: {e}")
def setup_push_schedule(self, frequency: float, target_url: str):
"""设置或更新推送计划"""
if not isinstance(frequency, (int, float)) or frequency <= 0:
raise ValueError("Frequency must be a positive number (pushes per second).")
self.target_url = str(target_url)
interval_seconds = 1.0 / frequency
if not self.scheduler.running: # 确保调度器正在运行
try:
logger.info("APScheduler was not running. Attempting to start it now.")
self.scheduler.start()
except Exception as e:
logger.error(f"Failed to start APScheduler in setup_push_schedule: {e}")
raise RuntimeError(f"APScheduler could not be started: {e}")
try:
if self.scheduler.get_job(self.push_job_id):
self.scheduler.remove_job(self.push_job_id)
logger.info(f"Removed existing push job: {self.push_job_id}")
except Exception as e:
logger.error(f"Error removing existing job: {e}")
first_run_time = datetime.datetime.now() + datetime.timedelta(seconds=10)
self.scheduler.add_job(
self._push_data_task,
trigger='interval',
seconds=interval_seconds,
id=self.push_job_id,
next_run_time=first_run_time,
replace_existing=True
)
logger.info(f"Push task scheduled to {self.target_url} every {interval_seconds:.2f}s, starting in 10s.")
def stop_push_schedule(self):
"""停止数据推送任务"""
if self.scheduler.get_job(self.push_job_id):
try:
self.scheduler.remove_job(self.push_job_id)
logger.info(f"Push job {self.push_job_id} stopped successfully.")
self.target_url = None # 清除目标 URL
except Exception as e:
logger.error(f"Error stopping push job {self.push_job_id}: {e}")
else:
logger.info("No active push job to stop.")
def shutdown_scheduler(self):
"""安全关闭调度器"""
if self.scheduler.running:
try:
self.scheduler.shutdown()
logger.info("DataPusher's APScheduler shut down successfully.")
except Exception as e:
logger.error(f"Error shutting down DataPusher's APScheduler: {e}")
def push_specific_payload(self, payload: dict):
"""推送一个特定的、已格式化的数据负载到配置的 target_url。"""
if not self.target_url:
logger.warning("DataPusher: Target URL not set. Cannot push specific payload.")
return
if not payload:
logger.warning("DataPusher: Received empty payload for specific push. Skipping.")
return
logger.info(f"Attempting to push specific payload to {self.target_url}")
try:
response = requests.post(self.target_url, json=payload, timeout=10) # Increased timeout for one-off
response.raise_for_status()
logger.info(f"Specific payload pushed successfully to {self.target_url}. Status: {response.status_code}")
except requests.exceptions.RequestException as e:
logger.error(f"Error pushing specific payload to {self.target_url}: {e}")
except Exception as e:
logger.error(f"An unexpected error occurred during specific payload push: {e}")
# 全局 DataPusher 实例,将由主应用初始化
data_pusher_instance: Optional[DataPusher] = None
# --- FastAPI Request Body Model ---
class PushConfigRequest(BaseModel):
frequency: float
url: HttpUrl
# --- FastAPI HTTP Endpoint (using APIRouter) ---
@pusher_router.post("/setup_push", summary="配置数据推送任务")
async def handle_setup_push(config: PushConfigRequest = Body(...)):
global data_pusher_instance
if data_pusher_instance is None:
# 这个错误理论上不应该发生,如果主应用正确初始化了 data_pusher_instance
logger.error("CRITICAL: /setup_push called but data_pusher_instance is None. Main app did not initialize it.")
raise HTTPException(status_code=503, detail="DataPusher service not available. Initialization may have failed.")
if config.frequency <= 0: # Pydantic v2 中可以直接用 gt=0
raise HTTPException(status_code=400, detail="Invalid frequency value. Must be a positive number.")
try:
data_pusher_instance.setup_push_schedule(config.frequency, str(config.url))
return {
"message": "Push task configured successfully.",
"frequency_hz": config.frequency,
"interval_seconds": 1.0 / config.frequency,
"target_url": str(config.url),
"first_push_delay_seconds": 10
}
except ValueError as ve:
raise HTTPException(status_code=400, detail=str(ve))
except RuntimeError as re: # 例如 APScheduler 启动失败
logger.error(f"Runtime error during push schedule setup: {re}")
raise HTTPException(status_code=500, detail=str(re))
except Exception as e:
logger.error(f"Error setting up push schedule: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@pusher_router.post("/stop_push", summary="停止当前数据推送任务")
async def handle_stop_push():
global data_pusher_instance
if data_pusher_instance is None:
logger.error("CRITICAL: /stop_push called but data_pusher_instance is None.")
raise HTTPException(status_code=503, detail="DataPusher service not available.")
try:
data_pusher_instance.stop_push_schedule()
return {"message": "Push task stopped successfully if it was running."}
except Exception as e:
logger.error(f"Error stopping push schedule: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Internal server error while stopping push: {str(e)}")
# --- Initialization and Shutdown Functions for Main App ---
def initialize_data_pusher(detector_instance_param): # Renamed to avoid conflict
"""
由主应用程序调用以创建和配置 DataPusher 实例。
"""
global data_pusher_instance
if data_pusher_instance is None:
logger.info("Initializing DataPusher instance...")
data_pusher_instance = DataPusher(detector_instance_param)
else:
logger.info("DataPusher instance already initialized. Updating detector instance if provided.")
data_pusher_instance.update_detector_instance(detector_instance_param)
return data_pusher_instance
def get_data_pusher_instance() -> Optional[DataPusher]:
"""获取 DataPusher 实例 (主要用于主应用可能需要访问它的其他方法,如 shutdown)"""
return data_pusher_instance
# 移除 if __name__ == '__main__' 和 run_pusher_service因为不再独立运行
# 示例代码可以移至主应用的文档或测试脚本中。
# 注意:
# RFDETRDetector 实例的生命周期由 api_server.py (current_detector) 管理。
# 当 api_server.py 中的模型切换时,需要有一种机制来更新 DataPusher 内部的 detector 引用。
# initialize_data_pusher 可以被多次调用 (例如,在模型切换后),它会更新 DataPusher 持有的 detector 实例。