Files
VisDrone-Version/data_pusher.py

249 lines
12 KiB
Python
Raw Normal View History

2025-08-05 16:55:45 +08:00
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 实例。