""" AI Box Sophon Adapter — MQTT 心跳上报处理器 监听 MQTT 主题 /board_ping(通过 MQTT Bridge 路由到 chongming/aibox_sophon_adapter/board_ping), 解析设备心跳数据并更新数据库记录。 """ import orjson import logging from datetime import datetime from chongming_worker import rpc_method from ..models.board_models import BoardPingRequest logger = logging.getLogger(__name__) @rpc_method( subject="aibox_sophon_adapter.board_ping", method="MQTT", summary="AI Box 设备心跳上报", request_model=BoardPingRequest, ) async def board_ping(request: BoardPingRequest, worker_id: str = "") -> dict: """ 处理设备心跳上报 设备通过 MQTT 主题 /board_ping 定期(默认 5 秒)上报系统状态。 收到心跳后更新数据库中的设备记录,标记为在线。 """ from ..global_var import ( db, heartbeat_timeout, stream_manager, link_path, mqtt_forward, ffmpeg_available, ) board_id = request.BoardId ping_json = request.model_dump_json() # ---- 心跳落库(核心步骤,失败才视为心跳处理失败) ---- try: if db is None: raise RuntimeError("Database not initialized") async with db.get_session() as session: from ..models.orm import BoardHeartbeat from sqlalchemy import select stmt = select(BoardHeartbeat).where(BoardHeartbeat.board_id == board_id) # type: ignore result = await session.execute(stmt) record: BoardHeartbeat | None = result.scalar_one_or_none() now = datetime.now() if record is None: # 新设备,创建记录 record = BoardHeartbeat( board_id=board_id, board_ip=request.BoardIp, board_platform=request.BoardPlatform, board_temp=request.BoardTemp, board_type=request.BoardType, status=request.Status, version=request.Version, system_info=request.System, grant_code=request.GrantCode, grant_desc=request.GrantDesc, last_ping_time=now, ping_data=ping_json, is_online=True, ) session.add(record) logger.info(f"[Heartbeat] 新设备上线: {board_id} (IP: {request.BoardIp})") else: # 已有设备,更新心跳 record.board_ip = request.BoardIp record.board_platform = request.BoardPlatform record.board_temp = request.BoardTemp record.board_type = request.BoardType record.status = request.Status record.version = request.Version record.system_info = request.System record.grant_code = request.GrantCode record.grant_desc = request.GrantDesc record.last_ping_time = now record.ping_data = ping_json record.is_online = True record.updated_at = now await session.commit() except Exception as e: logger.error(f"[Heartbeat] 处理心跳失败 (board_id={board_id}): {e}") return {"error": str(e)} # ---- 以下为可选能力:失败只告警,不阻断心跳 ---- ping_data = orjson.loads(ping_json.encode("utf-8")) # RTSP 拉流(依赖 ffmpeg;缺失时跳过并在启动日志提示) medias = ping_data.get("Medias", {}) pending = [ media for media in medias if "正常" in media.get("MediaStatus", {}).get("label", "") ] if pending and stream_manager is None: logger.warning("[Heartbeat] StreamManager 未初始化,跳过 RTSP 拉流") elif pending and not ffmpeg_available: logger.warning("[Heartbeat] ffmpeg 不可用,跳过 RTSP 拉流(安装 ffmpeg 后恢复)") else: for media in pending: stream_id = media["MediaName"] source_url = media["MediaUrl"].replace("/Link", link_path) try: await stream_manager.start_stream(f"/{stream_id}", source_url) # type: ignore except Exception as e: logger.warning(f"[Heartbeat] 通道 {stream_id} 拉流失败: {e}") # 转发心跳数据到外部 MQTT Broker if mqtt_forward is not None: success = await mqtt_forward.publish(ping_data) if not success: logger.warning("[MQTTForward] 心跳转发失败") else: logger.warning(f"[MQTTForward] 心跳已转发 ({len(ping_data)} keys)") else: logger.warning("[MQTTForward] mqtt_forward 客户端为 None,跳过转发") logger.debug( f"[Heartbeat] {board_id}: temp={request.BoardTemp}, " f"disk={request.HostDisk.Used}/{request.HostDisk.Total} kB, " f"mem={request.HostMemory[0]:.1f} kB ({request.HostMemory[1]*100:.1f}%), " f"medias={len(request.Medias)}, tasks={len(request.Tasks)}" ) return {"received": True, "board_id": board_id}