Files
chongming-plugins/aibox_sophon_adapter/edge_app_controller.py
T
2026-08-10 11:29:54 +08:00

223 lines
7.9 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 _reply(-1, "缺少 command 字段")
handler = _COMMANDS.get(str(command))
if handler is None:
logger.warning(f"[EdgeAppController] 未知命令: {command}")
return _reply(-2, f"未知命令: {command}")
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 _reply(-3, f"命令执行异常: {e}")
# ============================================================
# 内置命令(示例)
# ============================================================
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,
internal_mqtt,
internal_request_topic,
internal_response_topic,
)
board_id = payload.get("BoardId")
if not board_id:
return _reply(-4, "缺少 BoardId 参数")
print(f"[alg_media_fetch]Processing for board: {board_id}")
print(f"[alg_media_fetch]Expected payload: {payload}")
if db is None:
return _reply(-5, "数据库未初始化")
if internal_mqtt is None:
return _reply(-5, "internal_mqtt 未初始化")
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 _reply(-4, f"设备 {board_id} 不在数据库中")
# 内部请求-响应主题由 [workers.config.internal_mqtt] 配置,与盒子侧约定一致
# (盒子只设根主题 box,子路径固定 aibox_sophon/...,回复主题固定为 ..._reply)。
# 勿用 chongming/{插件}/{操作}:网关 Bridge 订阅 chongming/+/+,会拦截
# 3 级主题并按 Worker RPC 路由,而 edge_app_controller 不是 @rpc_method
# (Worker 无对应 Queryable),会导致 worker_manager 报 "无回复"。
resp = await internal_mqtt.request(
request_topic=internal_request_topic,
response_topic=internal_response_topic,
payload=payload,
timeout=5
)
print(f"[alg_media_fetch]Response from internal_mqtt: {resp}")
if resp is None:
return _reply(-5, "获取算法媒体信息失败(内部请求无响应)")
return resp
async def _cmd_alg_task_fetch(payload: dict) -> dict | None:
"""
算法任务抓取:按 BoardId 返回设备最近一次心跳中的算法任务信息
请求: {"Event": "/alg_task_fetch", "BoardId": "..."}
返回: {"board_id": ..., "tasks": [...]}
"""
from .global_var import (
db,
internal_mqtt,
internal_request_topic,
internal_response_topic,
)
board_id = payload.get("BoardId")
if not board_id:
return _reply(-4, "缺少 BoardId 参数")
print(f"[alg_task_fetch]Processing for board: {board_id}")
print(f"[alg_task_fetch]Expected payload: {payload}")
if db is None:
return _reply(-5, "数据库未初始化")
if internal_mqtt is None:
return _reply(-5, "internal_mqtt 未初始化")
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 _reply(-4, f"设备 {board_id} 不在数据库中")
resp = await internal_mqtt.request(
request_topic=internal_request_topic,
response_topic=internal_response_topic,
payload=payload,
timeout=5
)
return resp
# 命令注册表: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,
}