223 lines
7.4 KiB
Python
223 lines
7.4 KiB
Python
"""
|
||
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,
|
||
internal_mqtt,
|
||
internal_request_topic,
|
||
internal_response_topic,
|
||
)
|
||
|
||
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:
|
||
return None
|
||
if internal_mqtt is None:
|
||
return None
|
||
|
||
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
|
||
|
||
# 内部请求-响应主题由 [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 None
|
||
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 None
|
||
|
||
print(f"[alg_task_fetch]Processing for board: {board_id}")
|
||
print(f"[alg_task_fetch]Expected payload: {payload}")
|
||
|
||
if db is None:
|
||
return None
|
||
if internal_mqtt is None:
|
||
return None
|
||
|
||
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
|
||
|
||
|
||
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,
|
||
}
|