modify: aibox_sophon_adapter plugins
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
AI Box Sophon Adapter 插件
|
||||
|
||||
基于 MQTT 的 AI Box 设备心跳适配器,实现:
|
||||
1. 通过 MQTT 主题 /board_ping 接收设备心跳上报
|
||||
2. 解析设备系统参数(温度、硬盘、内存、通道、任务、TPU 等)
|
||||
3. 持久化心跳数据到 SQLite 数据库
|
||||
4. 提供 HTTP API 查询设备在线状态和详情
|
||||
5. 后台定时检查离线设备,触发异常告警
|
||||
|
||||
心跳超时机制:
|
||||
- 设备按配置间隔(默认 5 秒)上报心跳
|
||||
- 服务端每 check_interval 秒检查一次
|
||||
- 超过 heartbeat_timeout 秒未收到心跳的设备标记为离线
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from chongming_worker import auto_bind_models
|
||||
from chongming_worker.runtime import create_workers as _create_workers
|
||||
|
||||
from .router import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 自动绑定 Pydantic 模型(用于 gen_models 生成的模型)
|
||||
auto_bind_models("aibox_sophon_adapter")
|
||||
|
||||
# 离线检查任务引用
|
||||
_offline_check_task: asyncio.Task | None = None
|
||||
|
||||
|
||||
async def _check_offline_devices(interval: float):
|
||||
"""
|
||||
后台任务:定期检查离线设备
|
||||
|
||||
每隔 interval 秒扫描数据库,将超过 heartbeat_timeout 秒
|
||||
未收到心跳的设备标记为离线,并输出告警日志。
|
||||
"""
|
||||
from .global_var import db, heartbeat_timeout
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(interval)
|
||||
try:
|
||||
if db is None:
|
||||
continue
|
||||
|
||||
async with db.get_session() as session:
|
||||
from .models.orm import BoardHeartbeat
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = select(BoardHeartbeat).where(BoardHeartbeat.is_online == True)
|
||||
result = await session.execute(stmt)
|
||||
records = result.scalars().all()
|
||||
|
||||
now = datetime.now()
|
||||
offline_count = 0
|
||||
for record in records:
|
||||
elapsed = (now - record.last_ping_time).total_seconds()
|
||||
if elapsed > heartbeat_timeout:
|
||||
record.is_online = False
|
||||
record.updated_at = now
|
||||
offline_count += 1
|
||||
logger.warning(
|
||||
f"[Offline] 设备 {record.board_id} (IP: {record.board_ip}) "
|
||||
f"已离线 {elapsed:.0f} 秒(超时阈值: {heartbeat_timeout}s)"
|
||||
)
|
||||
|
||||
if offline_count > 0:
|
||||
await session.commit()
|
||||
logger.info(f"[Offline] 本轮标记 {offline_count} 个设备为离线")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[OfflineChecker] 检查异常: {e}")
|
||||
|
||||
|
||||
async def create_workers(session, config, count=1, worker_manager=None):
|
||||
"""
|
||||
插件入口 — 初始化数据库和全局资源,创建 Worker 副本
|
||||
|
||||
由 FastAPI lifespan 在启动时调用。
|
||||
"""
|
||||
from .global_var import set_global_var, heartbeat_timeout as _default_timeout
|
||||
from chongming_database import Database
|
||||
|
||||
# 读取心跳配置
|
||||
worker_config = config.get("worker", {}).get("heartbeat", {}) if isinstance(config, dict) else {}
|
||||
hb_interval = worker_config.get("interval", 5)
|
||||
hb_timeout = worker_config.get("timeout", _default_timeout)
|
||||
check_interval = worker_config.get("check_interval", 10)
|
||||
|
||||
# 设定全局变量
|
||||
global heartbeat_timeout
|
||||
heartbeat_timeout = hb_timeout
|
||||
|
||||
# 初始化数据库
|
||||
data_dir = Path(config.get("data_dir", "./data")) if isinstance(config, dict) else Path("./data")
|
||||
db_path = data_dir / "storage" / "chongming" / "aibox_sophon_adapter.db"
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
db = Database(db_path)
|
||||
await db.initialize()
|
||||
|
||||
# 创建表
|
||||
from .models.orm import BoardHeartbeat
|
||||
async with db.get_session() as sess:
|
||||
from sqlmodel import SQLModel
|
||||
await sess.run_sync(lambda s: SQLModel.metadata.create_all(s.bind))
|
||||
|
||||
set_global_var("db", db)
|
||||
set_global_var("heartbeat_timeout", hb_timeout)
|
||||
set_global_var("heartbeat_interval", hb_interval)
|
||||
|
||||
logger.info(
|
||||
f"[aibox_sophon_adapter] 已初始化: "
|
||||
f"心跳间隔={hb_interval}s, 超时={hb_timeout}s, 检查间隔={check_interval}s"
|
||||
)
|
||||
|
||||
# 启动离线设备检查后台任务
|
||||
global _offline_check_task
|
||||
_offline_check_task = asyncio.create_task(_check_offline_devices(check_interval))
|
||||
|
||||
# 创建 Worker 副本
|
||||
return _create_workers(session, config, count, worker_manager)
|
||||
|
||||
|
||||
async def cleanup(worker_item: str):
|
||||
"""
|
||||
清理资源
|
||||
|
||||
在 Worker 关闭时调用,关闭数据库连接和后台任务。
|
||||
"""
|
||||
from .global_var import db
|
||||
|
||||
global _offline_check_task
|
||||
if _offline_check_task is not None and not _offline_check_task.done():
|
||||
_offline_check_task.cancel()
|
||||
try:
|
||||
await _offline_check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
logger.info("[aibox_sophon_adapter] 离线检查任务已停止")
|
||||
|
||||
if db is not None:
|
||||
await db.dispose()
|
||||
|
||||
logger.info(f"[aibox_sophon_adapter] {worker_item} 清理完毕")
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
AI Box Sophon Adapter — 全局变量
|
||||
|
||||
模块级共享资源管理。
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from chongming_database import Database
|
||||
|
||||
# 数据库实例
|
||||
db: Database | None = None
|
||||
|
||||
# 心跳超时配置(秒)
|
||||
heartbeat_timeout: float = 15.0
|
||||
|
||||
# 心跳上报间隔(秒)
|
||||
heartbeat_interval: float = 5.0
|
||||
|
||||
|
||||
def set_global_var(name: str, value: Any) -> None:
|
||||
"""
|
||||
设置当前模块的全局变量。
|
||||
|
||||
Args:
|
||||
name: 变量名(字符串)
|
||||
value: 任意值
|
||||
"""
|
||||
globals()[name] = value
|
||||
|
||||
|
||||
__all__ = [
|
||||
"db",
|
||||
"heartbeat_timeout",
|
||||
"heartbeat_interval",
|
||||
"set_global_var",
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
模型导出 — aibox_sophon_adapter
|
||||
|
||||
包含 gen_models 自动生成的模型和手动定义的 board_models。
|
||||
"""
|
||||
from .pydantic import (
|
||||
AiboxSophonAdapterResponse,
|
||||
GetDeviceGetRequest,
|
||||
GetDeviceGetResponse,
|
||||
GetOfflineDevicesGetResponse,
|
||||
ListDevicesGetResponse,
|
||||
)
|
||||
from .board_models import (
|
||||
BoardPingRequest,
|
||||
HostDiskInfo,
|
||||
MediaInfo,
|
||||
MediaStatusInfo,
|
||||
AlgItemInfo,
|
||||
AlgTaskStatusInfo,
|
||||
TaskInfo,
|
||||
TpuInfo,
|
||||
DeviceSummary,
|
||||
)
|
||||
from .orm import BoardHeartbeat
|
||||
|
||||
__all__ = [
|
||||
# 自动生成
|
||||
"AiboxSophonAdapterResponse",
|
||||
"GetDeviceGetRequest",
|
||||
"GetDeviceGetResponse",
|
||||
"GetOfflineDevicesGetResponse",
|
||||
"ListDevicesGetResponse",
|
||||
# 手动定义
|
||||
"BoardPingRequest",
|
||||
"HostDiskInfo",
|
||||
"MediaInfo",
|
||||
"MediaStatusInfo",
|
||||
"AlgItemInfo",
|
||||
"AlgTaskStatusInfo",
|
||||
"TaskInfo",
|
||||
"TpuInfo",
|
||||
"DeviceSummary",
|
||||
# ORM
|
||||
"BoardHeartbeat",
|
||||
]
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
AI Box Sophon Adapter — 设备心跳数据 Pydantic 模型(手动定义)
|
||||
|
||||
定义 BoardPingRequest 及其嵌套子结构。
|
||||
这些模型无法通过 gen_models 自动生成(嵌套层次过深),需要手动维护。
|
||||
|
||||
对应 MQTT 主题 /board_ping 的上报数据结构。
|
||||
|
||||
命名约定:
|
||||
- Pydantic 模型类名以 Info 结尾(如 HostDiskInfo),避免与 JSON 字段名冲突
|
||||
- 字段名直接对应 JSON key,无需 alias
|
||||
"""
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 心跳数据子结构
|
||||
# ============================================================
|
||||
|
||||
class HostDiskInfo(BaseModel):
|
||||
"""设备硬盘使用情况"""
|
||||
Available: int = Field(..., description="可用空间 (kB)")
|
||||
Total: int = Field(..., description="总空间 (kB)")
|
||||
Used: int = Field(..., description="已用空间 (kB)")
|
||||
|
||||
|
||||
class MediaStatusInfo(BaseModel):
|
||||
"""通道状态描述"""
|
||||
label: str = Field(..., description="状态标签,如 '正常'")
|
||||
type: int = Field(..., description="状态类型码")
|
||||
|
||||
|
||||
class MediaInfo(BaseModel):
|
||||
"""设备通道信息"""
|
||||
MediaName: str = Field(..., description="通道名称")
|
||||
MediaStatus: MediaStatusInfo = Field(..., description="通道状态")
|
||||
MediaUrl: str = Field(..., description="通道视频流地址")
|
||||
SubId: str = Field(default="", description="关联 GB28181 通道号")
|
||||
|
||||
|
||||
class AlgItemInfo(BaseModel):
|
||||
"""算法配置信息"""
|
||||
lineDesc: str = Field(default="", description="线描述")
|
||||
lineRequired: bool = Field(default=False, description="是否需要线")
|
||||
majorId: int = Field(..., description="主 ID")
|
||||
majorOnly: bool = Field(default=False, description="仅主算法")
|
||||
minorId: int = Field(..., description="副 ID")
|
||||
name: str = Field(..., description="算法名称")
|
||||
zoneDesc: str = Field(default="", description="区域描述")
|
||||
zoneRequired: bool = Field(default=False, description="是否需要区域")
|
||||
|
||||
|
||||
class AlgTaskStatusInfo(BaseModel):
|
||||
"""任务状态描述"""
|
||||
label: str = Field(..., description="状态标签,如 '运行中'")
|
||||
type: int = Field(..., description="状态类型码")
|
||||
|
||||
|
||||
class TaskInfo(BaseModel):
|
||||
"""设备任务信息"""
|
||||
AlgItem: list[AlgItemInfo] = Field(default_factory=list, description="算法配置列表")
|
||||
AlgTaskSession: str = Field(..., description="任务号")
|
||||
AlgTaskStatus: AlgTaskStatusInfo = Field(..., description="任务状态")
|
||||
|
||||
|
||||
class TpuInfo(BaseModel):
|
||||
"""TPU 算力资源使用情况"""
|
||||
mem_total: int = Field(..., description="内存总量")
|
||||
mem_usage: int = Field(..., description="内存使用量")
|
||||
tpu_usage: int = Field(..., description="TPU 使用率")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 心跳请求体(MQTT 上报的完整数据结构)
|
||||
# ============================================================
|
||||
|
||||
class BoardPingRequest(BaseModel):
|
||||
"""
|
||||
AI Box 设备心跳上报数据
|
||||
|
||||
设备通过 MQTT 主题 /board_ping 定期上报。
|
||||
上报间隔默认 5 秒。
|
||||
|
||||
包含字段:设备标识、IP、温度、硬盘、内存、GPS、
|
||||
通道列表、任务列表、TPU 算力等。
|
||||
"""
|
||||
BoardId: str = Field(..., description="当前盒子唯一标识")
|
||||
BoardIp: str = Field(..., description="设备 IP 地址")
|
||||
BoardPlatform: str = Field(..., description="当前设备硬件类型,如 'BM-BOX'")
|
||||
BoardTemp: str = Field(..., description="当前设备芯片温度,如 '44 (C)'")
|
||||
BoardType: str = Field(..., description="当前设备接入方式 WAN/4G")
|
||||
GBClientId: str = Field(default="", description="GB28181 设备号,配置后透传")
|
||||
GrantCode: int = Field(..., description="授权码")
|
||||
GrantDesc: str = Field(..., description="授权描述,如 '已授权'")
|
||||
HostDisk: HostDiskInfo = Field(..., description="当前设备硬盘情况")
|
||||
HostMemory: list[float] = Field(..., description="当前设备内存使用 [已使用(kB), 总利用率]")
|
||||
Key: str = Field(..., description="运行状态关键字,如 '运行中'")
|
||||
Latitude: float = Field(default=0.0, description="GPS 纬度")
|
||||
Longitude: float = Field(default=0.0, description="GPS 经度")
|
||||
Malloc: int = Field(..., description="当前应用 VmRSS 信息 (kB)")
|
||||
Medias: list[MediaInfo] = Field(default_factory=list, description="当前设备通道信息")
|
||||
Pid: int = Field(..., description="本次应用 PID")
|
||||
Se6ip: str = Field(default="", description="特定设备时透传主控端 IP")
|
||||
Status: str = Field(..., description="设备状态,如 '在线'")
|
||||
System: str = Field(..., description="当前设备系统版本")
|
||||
Tasks: list[TaskInfo] = Field(default_factory=list, description="当前设备任务信息")
|
||||
Time: int = Field(..., description="当前时间戳(毫秒)")
|
||||
Tpu: list[TpuInfo] = Field(default_factory=list, description="当前设备算力资源使用情况")
|
||||
Version: str = Field(..., description="软件版本")
|
||||
angleCourse: float = Field(default=0.0, description="GPS 角度")
|
||||
kSpeed: float = Field(default=0.0, description="GPS 速度 (km/h)")
|
||||
nSpeed: float = Field(default=0.0, description="GPS 速度 (m/s)")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 设备状态摘要(API 响应用)
|
||||
# ============================================================
|
||||
|
||||
class DeviceSummary(BaseModel):
|
||||
"""设备状态摘要(用于列表 API 响应)"""
|
||||
board_id: str = Field(..., description="设备唯一标识")
|
||||
board_ip: str = Field(..., description="设备 IP")
|
||||
board_platform: str = Field(..., description="硬件类型")
|
||||
board_type: str = Field(..., description="接入方式")
|
||||
board_temp: str = Field(..., description="芯片温度")
|
||||
status: str = Field(..., description="设备状态")
|
||||
version: str = Field(..., description="软件版本")
|
||||
is_online: bool = Field(..., description="是否在线")
|
||||
last_ping_time: Optional[str] = Field(default=None, description="最后心跳时间")
|
||||
grant_code: int = Field(..., description="授权码")
|
||||
grant_desc: str = Field(..., description="授权描述")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
AI Box Sophon Adapter — ORM 模型
|
||||
|
||||
SQLModel 定义设备心跳记录表。
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class BoardHeartbeat(SQLModel, table=True):
|
||||
"""设备心跳记录表"""
|
||||
__tablename__ = "board_heartbeat"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
board_id: str = Field(index=True, unique=True, description="设备唯一标识 (BoardId)")
|
||||
board_ip: str = Field(default="", description="设备 IP")
|
||||
board_platform: str = Field(default="", description="硬件类型")
|
||||
board_temp: str = Field(default="", description="芯片温度")
|
||||
board_type: str = Field(default="", description="接入方式 WAN/4G")
|
||||
status: str = Field(default="未知", description="设备状态")
|
||||
version: str = Field(default="", description="软件版本")
|
||||
system_info: str = Field(default="", description="系统版本信息")
|
||||
grant_code: int = Field(default=0, description="授权码")
|
||||
grant_desc: str = Field(default="", description="授权描述")
|
||||
last_ping_time: datetime = Field(default_factory=datetime.now, description="最后心跳时间")
|
||||
ping_data: str = Field(default="{}", description="完整心跳数据 JSON")
|
||||
is_online: bool = Field(default=True, description="是否在线")
|
||||
created_at: datetime = Field(default_factory=datetime.now, description="首次上报时间")
|
||||
updated_at: datetime = Field(default_factory=datetime.now, description="最后更新时间")
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
自动生成的 Pydantic 模型 — aibox_sophon_adapter
|
||||
来源: config/workers/aibox_sophon_adapter/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class GetDeviceGetRequest(BaseModel):
|
||||
"""查询单个设备详情 — 请求体"""
|
||||
board_id: str = Field(...)
|
||||
|
||||
class GetDeviceGetResponse(BaseModel):
|
||||
"""查询单个设备详情 — 响应体"""
|
||||
board_id: str = Field(...)
|
||||
is_online: bool = Field(...)
|
||||
last_ping_time: Optional[str] = Field(default=None)
|
||||
ping_data: Optional[dict[str, Any]] = Field(default=None)
|
||||
|
||||
class GetOfflineDevicesGetResponse(BaseModel):
|
||||
"""查询离线/异常设备列表 — 响应体"""
|
||||
offline_devices: list[dict] = Field(...)
|
||||
total: int = Field(...)
|
||||
|
||||
class ListDevicesGetResponse(BaseModel):
|
||||
"""查询所有设备状态 — 响应体"""
|
||||
devices: list[dict] = Field(...)
|
||||
total: int = Field(...)
|
||||
|
||||
|
||||
# 所有响应模型的联合类型
|
||||
AiboxSophonAdapterResponse = ListDevicesGetResponse | GetOfflineDevicesGetResponse | GetDeviceGetResponse
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
路由导出 — aibox_sophon_adapter
|
||||
"""
|
||||
from .board_ping import board_ping
|
||||
from .list_devices import list_devices
|
||||
from .get_device import get_device
|
||||
from .get_offline_devices import get_offline_devices
|
||||
|
||||
__all__ = [
|
||||
"board_ping",
|
||||
"list_devices",
|
||||
"get_device",
|
||||
"get_offline_devices",
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
AI Box Sophon Adapter — MQTT 心跳上报处理器
|
||||
|
||||
监听 MQTT 主题 /board_ping(通过 MQTT Bridge 路由到 chongming/aibox_sophon_adapter/board_ping),
|
||||
解析设备心跳数据并更新数据库记录。
|
||||
"""
|
||||
import json
|
||||
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
|
||||
|
||||
board_id = request.BoardId
|
||||
|
||||
try:
|
||||
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)
|
||||
result = await session.execute(stmt)
|
||||
record: BoardHeartbeat | None = result.scalar_one_or_none()
|
||||
|
||||
now = datetime.now()
|
||||
ping_json = request.model_dump_json()
|
||||
|
||||
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()
|
||||
|
||||
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)}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Heartbeat] 处理心跳失败 (board_id={board_id}): {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
return {"received": True, "board_id": board_id}
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
查询单个设备详情 — GET /aibox/get_device?board_id=xxx
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="aibox_sophon_adapter.get_device",
|
||||
method="GET",
|
||||
summary="查询单个设备详情",
|
||||
)
|
||||
async def get_device(board_id: str, worker_id: str = "") -> dict:
|
||||
"""根据 BoardId 查询设备的完整心跳数据和在线状态"""
|
||||
from ..global_var import db, heartbeat_timeout
|
||||
|
||||
try:
|
||||
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)
|
||||
result = await session.execute(stmt)
|
||||
record: BoardHeartbeat | None = result.scalar_one_or_none()
|
||||
|
||||
if record is None:
|
||||
return {"error": f"设备不存在: {board_id}"}
|
||||
|
||||
now = datetime.now()
|
||||
elapsed = (now - record.last_ping_time).total_seconds()
|
||||
is_online = elapsed <= heartbeat_timeout
|
||||
|
||||
ping_data = {}
|
||||
try:
|
||||
ping_data = json.loads(record.ping_data) if record.ping_data else {}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"board_id": record.board_id,
|
||||
"board_ip": record.board_ip,
|
||||
"board_platform": record.board_platform,
|
||||
"board_type": record.board_type,
|
||||
"board_temp": record.board_temp,
|
||||
"status": record.status,
|
||||
"version": record.version,
|
||||
"system_info": record.system_info,
|
||||
"grant_code": record.grant_code,
|
||||
"grant_desc": record.grant_desc,
|
||||
"is_online": is_online,
|
||||
"last_ping_time": record.last_ping_time.isoformat() if record.last_ping_time else None,
|
||||
"elapsed_seconds": round(elapsed, 1),
|
||||
"ping_data": ping_data,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[get_device] 查询失败 (board_id={board_id}): {e}")
|
||||
return {"error": str(e)}
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
查询离线/异常设备 — GET /aibox/get_offline_devices
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="aibox_sophon_adapter.get_offline_devices",
|
||||
method="GET",
|
||||
summary="查询离线/异常设备列表",
|
||||
)
|
||||
async def get_offline_devices(worker_id: str = "") -> dict:
|
||||
"""返回所有超过心跳超时时间未上报的设备列表"""
|
||||
from ..global_var import db, heartbeat_timeout
|
||||
|
||||
offline_devices = []
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
from ..models.orm import BoardHeartbeat
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = select(BoardHeartbeat).order_by(BoardHeartbeat.last_ping_time.desc())
|
||||
result = await session.execute(stmt)
|
||||
records = result.scalars().all()
|
||||
|
||||
now = datetime.now()
|
||||
for record in records:
|
||||
elapsed = (now - record.last_ping_time).total_seconds()
|
||||
is_online = elapsed <= heartbeat_timeout
|
||||
|
||||
if not is_online:
|
||||
offline_devices.append({
|
||||
"board_id": record.board_id,
|
||||
"board_ip": record.board_ip,
|
||||
"board_platform": record.board_platform,
|
||||
"board_type": record.board_type,
|
||||
"status": record.status,
|
||||
"version": record.version,
|
||||
"last_ping_time": record.last_ping_time.isoformat() if record.last_ping_time else None,
|
||||
"elapsed_seconds": round(elapsed, 1),
|
||||
"is_online": False,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[get_offline_devices] 查询失败: {e}")
|
||||
return {"offline_devices": [], "total": 0, "error": str(e)}
|
||||
|
||||
return {"offline_devices": offline_devices, "total": len(offline_devices)}
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
查询所有设备状态 — GET /aibox/list_devices
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="aibox_sophon_adapter.list_devices",
|
||||
method="GET",
|
||||
summary="查询所有设备状态",
|
||||
)
|
||||
async def list_devices(worker_id: str = "") -> dict:
|
||||
"""返回所有已上报过心跳的设备及其在线状态"""
|
||||
from ..global_var import db, heartbeat_timeout
|
||||
|
||||
devices = []
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
from ..models.orm import BoardHeartbeat
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = select(BoardHeartbeat).order_by(BoardHeartbeat.updated_at.desc())
|
||||
result = await session.execute(stmt)
|
||||
records = result.scalars().all()
|
||||
|
||||
now = datetime.now()
|
||||
for record in records:
|
||||
# 检查是否超时
|
||||
elapsed = (now - record.last_ping_time).total_seconds()
|
||||
is_online = elapsed <= heartbeat_timeout
|
||||
|
||||
ping_data = {}
|
||||
try:
|
||||
ping_data = json.loads(record.ping_data) if record.ping_data else {}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
devices.append({
|
||||
"board_id": record.board_id,
|
||||
"board_ip": record.board_ip,
|
||||
"board_platform": record.board_platform,
|
||||
"board_type": record.board_type,
|
||||
"board_temp": record.board_temp,
|
||||
"status": record.status,
|
||||
"version": record.version,
|
||||
"system_info": record.system_info,
|
||||
"grant_code": record.grant_code,
|
||||
"grant_desc": record.grant_desc,
|
||||
"is_online": is_online,
|
||||
"last_ping_time": record.last_ping_time.isoformat() if record.last_ping_time else None,
|
||||
"elapsed_seconds": round(elapsed, 1),
|
||||
"host_disk": ping_data.get("HostDisk", {}),
|
||||
"host_memory": ping_data.get("HostMemory", []),
|
||||
"medias_count": len(ping_data.get("Medias", [])),
|
||||
"tasks_count": len(ping_data.get("Tasks", [])),
|
||||
"tpu": ping_data.get("Tpu", []),
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[list_devices] 查询失败: {e}")
|
||||
return {"devices": [], "total": 0, "error": str(e)}
|
||||
|
||||
return {"devices": devices, "total": len(devices)}
|
||||
Reference in New Issue
Block a user