2026-07-27 14:28:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
AI Box Sophon Adapter 插件
|
|
|
|
|
|
|
|
|
|
|
|
基于 MQTT 的 AI Box 设备心跳适配器,实现:
|
2026-08-14 16:04:39 +08:00
|
|
|
|
1. 通过 MQTT 主题接收设备心跳上报
|
2026-07-27 14:28:07 +08:00
|
|
|
|
2. 解析设备系统参数(温度、硬盘、内存、通道、任务、TPU 等)
|
|
|
|
|
|
3. 持久化心跳数据到 SQLite 数据库
|
|
|
|
|
|
4. 提供 HTTP API 查询设备在线状态和详情
|
|
|
|
|
|
5. 后台定时检查离线设备,触发异常告警
|
2026-07-28 14:16:00 +08:00
|
|
|
|
6. 视频上传接口 — 盒子告警时录制视频上传(FORM-DATA)
|
|
|
|
|
|
7. 告警上报接口 — 违规事件 JSON 报文字动解析入库
|
|
|
|
|
|
8. 告警查询接口 — 告警列表/详情查询
|
2026-07-27 14:28:07 +08:00
|
|
|
|
|
|
|
|
|
|
心跳超时机制:
|
|
|
|
|
|
- 设备按配置间隔(默认 5 秒)上报心跳
|
|
|
|
|
|
- 服务端每 check_interval 秒检查一次
|
|
|
|
|
|
- 超过 heartbeat_timeout 秒未收到心跳的设备标记为离线
|
2026-08-14 16:04:39 +08:00
|
|
|
|
|
|
|
|
|
|
生命周期: 基于 Plugin 基类统一管理(数据库/后台任务/清理钩子由基类处理),
|
|
|
|
|
|
MQTT 客户端启动块已抽取到 mqtt_clients 能力组件。
|
2026-07-27 14:28:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
from chongming_worker.plugin import Plugin
|
2026-07-27 14:28:07 +08:00
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
from .router import * # noqa: F401,F403 触发 @rpc_method 注册
|
|
|
|
|
|
from .routes import router # noqa: F401 自定义 FastAPI 路由(网关通过包属性自动发现)
|
2026-08-07 14:28:39 +08:00
|
|
|
|
from .edge_app_controller import edge_app_controller # noqa: F401
|
2026-08-14 16:04:39 +08:00
|
|
|
|
from .mqtt_clients import start_mqtt_forward, start_mqtt_subscribe, start_internal_mqtt
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
2026-07-27 14:28:07 +08:00
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
2026-07-27 14:28:07 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
stmt = select(BoardHeartbeat).where(BoardHeartbeat.is_online)
|
2026-07-27 14:28:07 +08:00
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
class AiboxSophonAdapterPlugin(Plugin):
|
|
|
|
|
|
"""AI Box 适配插件 — 统一生命周期。"""
|
2026-07-27 14:28:07 +08:00
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
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", {})
|
2026-08-07 14:28:39 +08:00
|
|
|
|
)
|
2026-08-14 16:04:39 +08:00
|
|
|
|
|
|
|
|
|
|
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"
|
2026-08-07 14:28:39 +08:00
|
|
|
|
)
|
2026-07-27 14:28:07 +08:00
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
# 后台任务由基类跟踪,cleanup 时自动取消
|
|
|
|
|
|
self.spawn_task(_check_offline_devices(check_interval))
|
2026-07-27 14:28:07 +08:00
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
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()
|
2026-07-27 14:28:07 +08:00
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
if mqtt_forward is not None:
|
|
|
|
|
|
await mqtt_forward.stop()
|
2026-07-27 14:28:07 +08:00
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
if mqtt_subscribe is not None:
|
|
|
|
|
|
await mqtt_subscribe.stop()
|
2026-08-03 09:16:23 +08:00
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
if internal_mqtt is not None:
|
|
|
|
|
|
await internal_mqtt.stop()
|
2026-08-03 15:06:29 +08:00
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
logger.info("[aibox_sophon_adapter] 清理完成")
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 16:04:39 +08:00
|
|
|
|
# 暴露给 app lifespan 的模块级入口(bound method,签名与旧写法完全一致)
|
|
|
|
|
|
# 注意: 实例名必须为 plugin(router 已被 routes.router APIRouter 占用)
|
|
|
|
|
|
plugin = AiboxSophonAdapterPlugin()
|
|
|
|
|
|
create_workers = plugin.create_workers
|
|
|
|
|
|
cleanup = plugin.cleanup
|