Files
chongming-plugins/aibox_sophon_adapter/__init__.py
T
2026-08-14 16:04:39 +08:00

188 lines
7.8 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.
"""
AI Box Sophon Adapter 插件
基于 MQTT 的 AI Box 设备心跳适配器,实现:
1. 通过 MQTT 主题接收设备心跳上报
2. 解析设备系统参数(温度、硬盘、内存、通道、任务、TPU 等)
3. 持久化心跳数据到 SQLite 数据库
4. 提供 HTTP API 查询设备在线状态和详情
5. 后台定时检查离线设备,触发异常告警
6. 视频上传接口 — 盒子告警时录制视频上传(FORM-DATA)
7. 告警上报接口 — 违规事件 JSON 报文字动解析入库
8. 告警查询接口 — 告警列表/详情查询
心跳超时机制:
- 设备按配置间隔(默认 5 秒)上报心跳
- 服务端每 check_interval 秒检查一次
- 超过 heartbeat_timeout 秒未收到心跳的设备标记为离线
生命周期: 基于 Plugin 基类统一管理(数据库/后台任务/清理钩子由基类处理),
MQTT 客户端启动块已抽取到 mqtt_clients 能力组件。
"""
import asyncio
import logging
from datetime import datetime
from pathlib import Path
from chongming_worker.plugin import Plugin
from .router import * # noqa: F401,F403 触发 @rpc_method 注册
from .routes import router # noqa: F401 自定义 FastAPI 路由(网关通过包属性自动发现)
from .edge_app_controller import edge_app_controller # noqa: F401
from .mqtt_clients import start_mqtt_forward, start_mqtt_subscribe, start_internal_mqtt
logger = logging.getLogger(__name__)
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)
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}")
class AiboxSophonAdapterPlugin(Plugin):
"""AI Box 适配插件 — 统一生命周期。"""
type = "aibox_sophon_adapter"
db_name = "aibox_sophon_adapter.db" # 自动在 data_dir 下建 SQLite 库
auto_bind = True # 首次初始化时自动 auto_bind_models
async def on_init(self):
"""首次 create_workers 时调用一次:初始化数据库客户端、流控制器、后台任务。"""
config = self.config
from .global_var import set_global_var, heartbeat_timeout as _default_timeout
from chongming_stream_manager import StreamManager
# 所有运行时配置均从 [workers.config] 及其子节读取
data_dir = Path(config.get("data_dir", "./data"))
mediamtx_addr = config.get("mediamtx_addr", "rtsp://127.0.0.1:8554")
link_path = config.get("link_path", "/data/bm-app")
# 心跳配置
heartbeat = config.get("heartbeat", {})
hb_interval = heartbeat.get("interval", 5)
hb_timeout = heartbeat.get("timeout", _default_timeout)
check_interval = heartbeat.get("check_interval", 10)
# 告警保留配置
alarm_retention = config.get("alarm_retention", {})
alarm_max = alarm_retention.get("max_records", 0)
# 豆包 AI 分析开关
doubao_cfg = config.get("doubao_analysis", {})
doubao_enabled = doubao_cfg.get("enabled", True)
# 告警转发配置
alarm_fwd_cfg = config.get("alarm_forward", {})
alarm_fwd_enabled = alarm_fwd_cfg.get("enabled", False)
alarm_fwd_url = alarm_fwd_cfg.get("url", "http://dg.datagoo.cn:60014/open-api/event-alarm/receive")
alarm_fwd_video_url = alarm_fwd_cfg.get("video_upload_url", "http://dg.datagoo.cn:60014/open-api/event-alarm/receiveVideoUpload")
alarm_fwd_timeout = alarm_fwd_cfg.get("timeout", 10.0)
# 数据库已由基类初始化(data_dir/aibox_sophon_adapter.db)
db = self.db
# 初始化视频流控制器
stream_manager = StreamManager(mediamtx_addr)
# upload 根目录(用于清理告警关联视频)
upload_root_path = str(data_dir / "uploads")
# MQTT 客户端能力组件(启动失败降级为 None,仅警告)
mqtt_fwd_client = await start_mqtt_forward(config.get("mqtt_forward", {}))
mqtt_sub_client = await start_mqtt_subscribe(config.get("mqtt_subscribe", {}))
int_mqtt_client, int_req_topic, int_resp_topic = await start_internal_mqtt(
config.get("internal_mqtt", {})
)
set_global_var("db", db)
set_global_var("heartbeat_timeout", hb_timeout)
set_global_var("heartbeat_interval", hb_interval)
set_global_var("stream_manager", stream_manager)
set_global_var("link_path", link_path)
set_global_var("mqtt_forward", mqtt_fwd_client)
set_global_var("mqtt_subscribe", mqtt_sub_client)
set_global_var("internal_mqtt", int_mqtt_client)
set_global_var("internal_request_topic", int_req_topic)
set_global_var("internal_response_topic", int_resp_topic)
set_global_var("upload_root", upload_root_path)
set_global_var("alarm_max_records", alarm_max)
set_global_var("doubao_analysis_enabled", doubao_enabled)
set_global_var("alarm_forward_enabled", alarm_fwd_enabled)
set_global_var("alarm_forward_url", alarm_fwd_url)
set_global_var("alarm_forward_video_url", alarm_fwd_video_url)
set_global_var("alarm_forward_timeout", alarm_fwd_timeout)
logger.info(
"[aibox_sophon_adapter] 已初始化: "
f"心跳间隔={hb_interval}s, 超时={hb_timeout}s, 检查间隔={check_interval}s"
)
# 后台任务由基类跟踪,cleanup 时自动取消
self.spawn_task(_check_offline_devices(check_interval))
async def on_cleanup(self):
"""cleanup 时调用:关闭流控制器与外部 MQTT 客户端(db 与后台任务由基类处理)。"""
from .global_var import (
stream_manager, mqtt_forward, mqtt_subscribe, internal_mqtt,
)
if stream_manager is not None:
await stream_manager.stop_all()
if mqtt_forward is not None:
await mqtt_forward.stop()
if mqtt_subscribe is not None:
await mqtt_subscribe.stop()
if internal_mqtt is not None:
await internal_mqtt.stop()
logger.info("[aibox_sophon_adapter] 清理完成")
# 暴露给 app lifespan 的模块级入口(bound method,签名与旧写法完全一致)
# 注意: 实例名必须为 plugin(router 已被 routes.router APIRouter 占用)
plugin = AiboxSophonAdapterPlugin()
create_workers = plugin.create_workers
cleanup = plugin.cleanup