""" 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 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("数据库未初始化") if internal_mqtt is None: raise RuntimeError("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 None resp = await internal_mqtt.request( request_topic="/chongming/aibox_sophon_adapter/edge_app_controller", response_topic="/chongming/aibox_sophon_adapter/edge_app_controller_reply", payload=payload, timeout=5 ) print(f"[alg_media_fetch]Response from internal_mqtt: {resp}") 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 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, }