Files
chongming-plugins/aibox_sophon_adapter/router/board_ping.py
T
2026-08-23 04:34:38 +08:00

131 lines
5.1 KiB
Python

"""
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)
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}