""" 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, }