2026-08-07 14:28:39 +08:00
|
|
|
|
"""
|
|
|
|
|
|
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:
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
handler = _COMMANDS.get(str(command))
|
|
|
|
|
|
if handler is None:
|
|
|
|
|
|
logger.warning(f"[EdgeAppController] 未知命令: {command}")
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
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}")
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 内置命令(示例)
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
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": [...]}
|
|
|
|
|
|
"""
|
2026-08-10 10:16:25 +08:00
|
|
|
|
from .global_var import (
|
|
|
|
|
|
db,
|
|
|
|
|
|
internal_mqtt,
|
|
|
|
|
|
internal_request_topic,
|
|
|
|
|
|
internal_response_topic,
|
|
|
|
|
|
)
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
board_id = payload.get("BoardId")
|
|
|
|
|
|
if not board_id:
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
print(f"[alg_media_fetch]Processing for board: {board_id}")
|
|
|
|
|
|
print(f"[alg_media_fetch]Expected payload: {payload}")
|
|
|
|
|
|
|
|
|
|
|
|
if db is None:
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 16:40:48 +08:00
|
|
|
|
if internal_mqtt is None:
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
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.")
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
2026-08-10 10:44:52 +08:00
|
|
|
|
# 内部请求-响应主题由 [workers.config.internal_mqtt] 配置,与盒子侧约定一致
|
|
|
|
|
|
# (盒子只设根主题 box,子路径固定 aibox_sophon/...,回复主题固定为 ..._reply)。
|
|
|
|
|
|
# 勿用 chongming/{插件}/{操作}:网关 Bridge 订阅 chongming/+/+,会拦截
|
|
|
|
|
|
# 3 级主题并按 Worker RPC 路由,而 edge_app_controller 不是 @rpc_method
|
|
|
|
|
|
# (Worker 无对应 Queryable),会导致 worker_manager 报 "无回复"。
|
2026-08-07 16:40:48 +08:00
|
|
|
|
resp = await internal_mqtt.request(
|
2026-08-10 10:16:25 +08:00
|
|
|
|
request_topic=internal_request_topic,
|
|
|
|
|
|
response_topic=internal_response_topic,
|
2026-08-07 16:40:48 +08:00
|
|
|
|
payload=payload,
|
|
|
|
|
|
timeout=5
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[alg_media_fetch]Response from internal_mqtt: {resp}")
|
|
|
|
|
|
|
2026-08-10 10:16:25 +08:00
|
|
|
|
if resp is None:
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 16:40:48 +08:00
|
|
|
|
return resp
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
async def _cmd_alg_task_fetch(payload: dict) -> dict | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
算法任务抓取:按 BoardId 返回设备最近一次心跳中的算法任务信息
|
|
|
|
|
|
|
|
|
|
|
|
请求: {"Event": "/alg_task_fetch", "BoardId": "..."}
|
|
|
|
|
|
返回: {"board_id": ..., "tasks": [...]}
|
|
|
|
|
|
"""
|
2026-08-10 11:29:54 +08:00
|
|
|
|
from .global_var import (
|
|
|
|
|
|
db,
|
|
|
|
|
|
internal_mqtt,
|
|
|
|
|
|
internal_request_topic,
|
|
|
|
|
|
internal_response_topic,
|
|
|
|
|
|
)
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
board_id = payload.get("BoardId")
|
|
|
|
|
|
if not board_id:
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
print(f"[alg_task_fetch]Processing for board: {board_id}")
|
|
|
|
|
|
print(f"[alg_task_fetch]Expected payload: {payload}")
|
|
|
|
|
|
|
|
|
|
|
|
if db is None:
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-10 11:29:54 +08:00
|
|
|
|
if internal_mqtt is None:
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
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.")
|
2026-08-11 14:47:19 +08:00
|
|
|
|
return None
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
2026-08-10 11:29:54 +08:00
|
|
|
|
|
|
|
|
|
|
resp = await internal_mqtt.request(
|
|
|
|
|
|
request_topic=internal_request_topic,
|
|
|
|
|
|
response_topic=internal_response_topic,
|
|
|
|
|
|
payload=payload,
|
|
|
|
|
|
timeout=5
|
|
|
|
|
|
)
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
2026-08-10 11:29:54 +08:00
|
|
|
|
return resp
|
2026-08-07 14:28:39 +08:00
|
|
|
|
|
|
|
|
|
|
# 命令注册表: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,
|
|
|
|
|
|
}
|