Files
chongming-plugins/aibox_sophon_adapter/edge_app_controller.py
T
2026-08-07 14:28:39 +08:00

208 lines
6.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.
"""
edge_app_controller — 外部 MQTT 请求处理(请求-响应协议)
订阅主题: /edge_app_controller
回复主题: /edge_app_controller_reply
请求消息 (JSON): {"command": "<命令名>", ...业务字段}
兼容协议: {"Event": "/alg_media_fetch", "BoardId": "..."}(Event 取最后一段作为命令)
回复消息 (JSON): {"code": 0, "msg": "ok", "data": {...}}
- code: 0 成功;非 0 失败
-1 缺少 command 字段
-2 未知命令
-3 命令执行异常
不回复约定:
命令处理函数返回 None 时,该命令只处理、不回复(静默命令,如心跳/日志类)。
适用于按命令/Event 控制是否回复,无需修改配置。
命令通过 _COMMANDS 注册表分发,新增命令只需在注册表登记一个 async 处理函数。
"""
import logging
from typing import Any, Callable, Coroutine
logger = logging.getLogger(__name__)
def _reply(code: int, msg: str, data: Any = None) -> dict:
"""统一回复封装"""
return {"code": code, "msg": msg, "data": data}
async def edge_app_controller(payload: dict, topic: str) -> dict | None:
"""
处理来自外部 Broker 的边缘应用控制器请求(订阅 /edge_app_controller)
Args:
payload: 请求消息(已解析为 dict),期望含 command 字段
topic: 实际命中的主题
Returns:
回复消息(统一封装 code / msg / data),由订阅客户端发布到 reply_topic;
命令函数返回 None 时返回 None(不回复)。
"""
command = (
payload.get("command")
or payload.get("cmd")
or payload.get("action")
or payload.get("Event")
)
# Event 可能是路径形式(如 /alg_media_fetch),取最后一段作为命令名
if isinstance(command, str) and command.startswith("/"):
command = command.rsplit("/", 1)[-1]
logger.info(
f"[EdgeAppController] 收到请求 topic={topic}, command={command}, payload={payload}"
)
if command is None:
return None
handler = _COMMANDS.get(str(command))
if handler is None:
logger.warning(f"[EdgeAppController] 未知命令: {command}")
return None
try:
data = await handler(payload)
# 命令函数返回 None 表示该命令只处理、不回复(如心跳/日志类静默命令)
if data is None:
logger.info(f"[EdgeAppController] 命令 {command} 处理完成(配置为不回复)")
return None
logger.info(f"[EdgeAppController] 命令 {command} 执行成功")
return data
except Exception as e:
logger.error(f"[EdgeAppController] 命令 {command} 执行失败: {e}")
return None
# ============================================================
# 内置命令(示例)
# ============================================================
async def _cmd_ping(payload: dict) -> dict | None:
"""存活探测:返回服务信息"""
return {"service": "aibox_sophon_adapter", "alive": True}
async def _cmd_ack(payload: dict) -> None:
"""
确认命令(示例):仅记录日志,不回复
返回 None 表示不回复 —— 需要"某些命令静默处理"时,让命令函数返回 None 即可。
"""
logger.info(f"[EdgeAppController] 命令 ack 已收到(不回复),payload={payload}")
return None
async def _cmd_query_status(payload: dict) -> dict | None:
"""
查询边缘应用/设备状态
复用插件已有的设备查询逻辑(router.list_devices),返回设备在线状态列表。
后续可按需扩展:查询指定应用运行状态、通道状态等。
"""
from .router.list_devices import list_devices
result = await list_devices()
return result
async def _cmd_alg_media_fetch(payload: dict) -> dict | None:
"""
算法媒体抓取:按 BoardId 返回设备最近一次心跳中的算法/通道(媒体)信息
请求: {"Event": "/alg_media_fetch", "BoardId": "..."}
返回: {"board_id": ..., "medias": [...], "tasks": [...]}
"""
from .global_var import db
board_id = payload.get("BoardId")
if not board_id:
return None
print(f"[alg_media_fetch]Processing for board: {board_id}")
print(f"[alg_media_fetch]Expected payload: {payload}")
if db is None:
raise RuntimeError("数据库未初始化")
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()
if record is None:
print(f"[alg_media_fetch]Board {board_id} not found in database.")
return None
import json
ping_data = {}
try:
ping_data = json.loads(record.ping_data) if record.ping_data else {}
except json.JSONDecodeError:
pass
return {
"board_id": board_id,
"board_ip": record.board_ip,
"medias": ping_data.get("Medias", []),
"tasks": ping_data.get("Tasks", []),
}
async def _cmd_alg_task_fetch(payload: dict) -> dict | None:
"""
算法任务抓取:按 BoardId 返回设备最近一次心跳中的算法任务信息
请求: {"Event": "/alg_task_fetch", "BoardId": "..."}
返回: {"board_id": ..., "tasks": [...]}
"""
from .global_var import db
board_id = payload.get("BoardId")
if not board_id:
return None
print(f"[alg_task_fetch]Processing for board: {board_id}")
print(f"[alg_task_fetch]Expected payload: {payload}")
if db is None:
raise RuntimeError("数据库未初始化")
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()
if record is None:
print(f"[alg_task_fetch]Board {board_id} not found in database.")
return None
import json
ping_data = {}
try:
ping_data = json.loads(record.ping_data) if record.ping_data else {}
except json.JSONDecodeError:
pass
return {
"board_id": board_id,
"tasks": ping_data.get("Tasks", []),
}
# 命令注册表:command -> async handler(payload) -> data(返回 None 表示不回复)
_COMMANDS: dict[str, Callable[[dict], Coroutine[Any, Any, Any]]] = {
"ping": _cmd_ping,
"ack": _cmd_ack,
"query_status": _cmd_query_status,
"alg_media_fetch": _cmd_alg_media_fetch,
"alg_task_fetch": _cmd_alg_task_fetch,
}