VWED_server/services/execution/handlers/robot_scheduling.py
2025-04-30 16:57:46 +08:00

784 lines
33 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.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
机器人调度处理器模块
提供与机器人调度操作相关的各种处理器
"""
import json
import asyncio
import aiohttp
import uuid
from typing import Dict, Any, List, Optional
from services.execution.task_context import TaskContext
from .base import BlockHandler, register_handler
from config.settings import settings
from utils.logger import get_logger
from .model.block_name import RobotBlockName
from typing import Tuple
# 获取日志记录器
logger = get_logger("services.execution.handlers.robot_scheduling")
# 提取公共的API调用函数
async def call_robot_api(api_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""
调用机器人调度服务API的通用函数
Args:
api_name: API名称对应API_ENDPOINTS中的键
params: API参数
Returns:
API响应结果
"""
# 获取API端点和方法
endpoint = settings.ROBOT_API_ENDPOINTS.get(api_name)
method = settings.ROBOT_API_METHODS.get(api_name)
if not endpoint or not method:
logger.error(f"未找到API端点或方法{api_name}")
return {
"success": False,
"message": f"未找到API配置: {api_name}"
}
# 检查是否启用测试模式
if settings.ROBOT_API_MOCK_MODE:
logger.info(f"[测试模式] 模拟调用API: {api_name}, 参数: {params}")
# 构造测试模式下的响应
test_response = generate_mock_response(api_name, params)
return test_response
# 构建完整的URL
url = f"{settings.ROBOT_API_BASE_URL}{endpoint}"
# 准备请求头
headers = {"Content-Type": "application/json"}
if settings.ROBOT_API_TOKEN:
headers["Authorization"] = f"Bearer {settings.ROBOT_API_TOKEN}"
logger.info(f"调用外部API {api_name} - {method} {url}, 参数: {params}")
try:
async with aiohttp.ClientSession() as session:
# 根据HTTP方法选择相应的请求方式
if method == "GET":
# 对于GET请求将params转换为URL参数
async with session.get(
url,
params=params,
headers=headers,
timeout=settings.ROBOT_API_TIMEOUT
) as response:
result = await response.json()
elif method == "POST":
# 对于POST请求将params作为JSON数据发送
async with session.post(
url,
json=params,
headers=headers,
timeout=settings.ROBOT_API_TIMEOUT
) as response:
result = await response.json()
elif method == "PUT":
# 对于PUT请求将params作为JSON数据发送
async with session.put(
url,
json=params,
headers=headers,
timeout=settings.ROBOT_API_TIMEOUT
) as response:
result = await response.json()
else:
logger.error(f"不支持的HTTP方法: {method}")
return {
"success": False,
"message": f"不支持的HTTP方法: {method}"
}
# 检查响应状态码
if response.status != 200:
logger.error(f"API调用失败: {url}, 状态码: {response.status}, 响应: {result}")
return {
"success": False,
"message": f"API调用失败, 状态码: {response.status}",
"data": result
}
logger.info(f"API调用成功: {url}, 响应: {result}")
return result
except aiohttp.ClientError as e:
logger.error(f"API调用客户端错误: {url}, 错误: {str(e)}")
return {
"success": False,
"message": f"API调用客户端错误: {str(e)}"
}
except asyncio.TimeoutError:
logger.error(f"API调用超时: {url}")
return {
"success": False,
"message": "API调用超时"
}
except json.JSONDecodeError:
logger.error(f"API响应解析失败: {url}")
return {
"success": False,
"message": "API响应格式错误无法解析JSON"
}
except Exception as e:
logger.error(f"API调用异常: {url}, 错误: {str(e)}")
return {
"success": False,
"message": f"API调用异常: {str(e)}"
}
def generate_mock_response(api_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""
生成测试模式下的模拟响应
Args:
api_name: API名称
params: API参数
Returns:
模拟的API响应
"""
# 基本响应结构
response = {
"success": True,
"message": f"[测试模式] {api_name} 操作成功",
"data": {}
}
# 根据不同API返回不同的模拟数据
if api_name == "agv_operation":
# 机器人通用动作
response["data"] = {
"success": True,
"taskId": f"MOCK-OP-{str(uuid.uuid4())[:8]}"
}
elif api_name == "select_agv":
# 选择执行机器人
response["data"] = {
"agvId": f"MOCK-AGV-{str(uuid.uuid4())[:8]}"
}
elif api_name == "vehicle_station":
# 获取机器人位置
response["data"] = {
"station": f"MOCK-STATION-{str(uuid.uuid4())[:5]}",
"lastStation": f"MOCK-LAST-STATION-{str(uuid.uuid4())[:5]}"
}
elif api_name == "get_battery_level":
# 获取机器人电量
response["data"] = {
"batteryLevel": float(f"{(uuid.uuid4().int % 100) / 100:.2f}")
}
elif api_name == "get_pgv_code":
# 获取机器人PGV码
response["data"] = {
"codeInfo": True if uuid.uuid4().int % 2 == 0 else False
}
# 记录模拟调用结果
logger.info(f"[测试模式] 生成模拟响应: {api_name}, 响应: {response}")
return response
class RobotBlockHandler(BlockHandler):
"""机器人调度处理器基类提供公共的API调用方法"""
async def _call_external_api(self, api_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""调用外部API的通用方法"""
return await call_robot_api(api_name, params)
def _analyze_affected_blocks(self, block: Dict[str, Any], current_block_id: str, current_block_name: str) -> List[Dict[str, Any]]:
"""
分析当前块的结构,找出受当前选择机器人块影响的所有下级块
Args:
block: 当前块的定义
current_block_id: 当前块的ID
current_block_name: 当前块的名称
Returns:
List[Dict[str, Any]]: 受影响的块列表每个元素包含块的ID、名称、类型和与当前块的关系
"""
affected_blocks = []
# 检查当前块是否有子块
if "children" in block and "default" in block["children"]:
children = block["children"]["default"]
logger.info(f"{current_block_name}(ID:{current_block_id}) 有 {len(children)} 个子块")
# 分析每个子块
for child in children:
child_id = child.get("id", "unknown")
child_name = child.get("name", f"b{child_id}")
child_type = child.get("blockType", "unknown")
# 记录子块信息
affected_blocks.append({
"id": child_id,
"name": child_name,
"type": child_type,
"relation": "direct_child",
"parent_id": current_block_id,
"parent_name": current_block_name
})
# 如果子块不是CSelectAgvBp类型则继续分析其子块
# 如果子块是CSelectAgvBp类型则停止分析该分支因为该分支将由子块的CSelectAgvBp负责
if child_type != "CSelectAgvBp" and "children" in child and "default" in child["children"]:
# 递归分析子块的子块
nested_affected = self._analyze_affected_blocks(child, child_id, child_name)
# 将所有嵌套子块添加到结果中
for nested_block in nested_affected:
# 添加到结果列表中,标记为嵌套关系
nested_block["relation"] = "nested_child"
affected_blocks.append(nested_block)
return affected_blocks
def _get_robot_id_for_block(self, block_id: str, block_name: str, context: TaskContext) -> Tuple[Optional[str], Optional[str]]:
"""
获取适用于当前块的机器人ID
首先检查是否有专门为该块设置的机器人ID变量
如果没有则尝试获取全局机器人ID
Args:
block_id: 当前块ID
block_name: 当前块名称
context: 任务上下文
Returns:
Optional[str]: 机器人ID如果没有找到则返回None
"""
# 首先检查是否有专门为该块设置的机器人ID
robot_id = context.get_variable(f"agv_for_block_{block_id}")
agv_task_id = context.get_variable(f"agv_task_id_{block_id}")
if robot_id:
logger.info(f"找到块 {block_name}(ID:{block_id}) 专用的机器人ID: {robot_id}")
return robot_id, agv_task_id
# 检查是否有按名称设置的机器人ID
robot_id = context.get_variable(f"agv_for_{block_name}")
if robot_id:
logger.info(f"找到块 {block_name} 专用的机器人ID: {robot_id}")
return robot_id, agv_task_id
# 如果没有专用设置尝试获取全局机器人ID
robot_id = context.get_variable("selectedAgvId")
if robot_id:
logger.info(f"使用全局机器人ID: {robot_id} 用于块 {block_name}(ID:{block_id})")
return robot_id, agv_task_id
# 如果仍未找到尝试从所有块输出中查找最近的selectedAgvId
# 这是兜底策略,保持向后兼容
for out_block_name, outputs in context.block_outputs.items():
if isinstance(outputs, dict) and "selectedAgvId" in outputs:
robot_id = outputs.get("selectedAgvId")
logger.info(f"从块 {out_block_name} 输出中获取机器人ID: {robot_id} 用于块 {block_name}(ID:{block_id})")
return robot_id, agv_task_id
# 未找到任何机器人ID
logger.warning(f"未找到块 {block_name}(ID:{block_id}) 可用的机器人ID")
return None, None
async def _update_task_record_agv_id(self, task_record_id: str, amr_id: str) -> None:
"""
更新任务记录中的agv_id字段
Args:
task_record_id: 任务记录ID
amr_id: 机器人ID
Returns:
None
"""
try:
from sqlalchemy.ext.asyncio import AsyncSession
from data.session import get_async_session
from data.models.taskrecord import VWEDTaskRecord
from sqlalchemy import select, update
if not amr_id:
logger.warning(f"未提供AMR ID无法更新任务记录 {task_record_id}")
return
# 将多个AMR ID用逗号连接
logger.info(f"准备更新任务 {task_record_id} 的机器人ID: {amr_id}")
# 先查询当前任务记录中是否已有agv_id
async with get_async_session() as session:
session: AsyncSession = session
# 查询当前记录
stmt = select(VWEDTaskRecord.agv_id).where(VWEDTaskRecord.id == task_record_id)
result = await session.execute(stmt)
current_agv_id = result.scalar_one_or_none()
# 确定最终要存储的agv_id值
if current_agv_id:
# 如果已经有值,需要合并并去重
logger.info(f"任务 {task_record_id} 当前已存在的机器人IDs: {current_agv_id}")
current_ids = current_agv_id.split(",")
all_ids = current_ids + [amr_id]
# 去重并过滤空值
unique_ids = list(set([id for id in all_ids if id]))
final_agv_id = ",".join(unique_ids)
logger.info(f"合并后的机器人IDs: {final_agv_id}")
else:
final_agv_id = amr_id
# 更新记录
stmt = update(VWEDTaskRecord).where(VWEDTaskRecord.id == task_record_id).values(agv_id=final_agv_id)
await session.execute(stmt)
await session.commit()
logger.info(f"成功更新任务记录 {task_record_id} 的agv_id字段: {final_agv_id}")
except Exception as e:
logger.error(f"更新任务记录 {task_record_id} 的agv_id字段时发生错误: {str(e)}")
# 机器人通用动作处理器
@register_handler(RobotBlockName.AGV_OPERATION)
class AgvOperationBlockHandler(RobotBlockHandler):
"""机器人通用动作处理器"""
async def execute(
self,
block: Dict[str, Any],
input_params: Dict[str, Any],
context: TaskContext
) -> Dict[str, Any]:
"""执行机器人通用动作操作"""
from services.sync_service import wait_for_task_block_action_completion
try:
# 获取关键参数用于验证
target_site_label = input_params.get("targetSiteLabel")
script_name = input_params.get("scriptName")
# 参数检查
if not target_site_label:
result = {
"success": False,
"message": "目标站点名不能为空"
}
await self._record_task_log(block, result, context)
return result
# 获取当前块信息
current_block_id = block.get("id", "unknown")
current_block_name = block.get("name", f"b{current_block_id}")
# 如果没有提供尝试获取适用于当前块的机器人ID
vehicle, agv_task_id = self._get_robot_id_for_block(current_block_id, current_block_name, context)
if vehicle:
# 设置到输入参数中
input_params["vehicle"] = vehicle
# 记录使用的机器人ID
if vehicle:
logger.info(f"执行机器人通用动作,块 {current_block_name}(ID:{current_block_id}) 使用机器人: {vehicle}, 目标站点: {target_site_label}")
else:
error_msg = f"执行机器人通用动作失败未指定机器人ID目标站点: {target_site_label}"
logger.error(error_msg)
result = {
"success": False,
"message": error_msg
}
await self._record_task_log(block, result, context)
return result
from services.sync_service import add_action
result = await add_action(
task_id=agv_task_id,
station_name=target_site_label,
action=script_name,
token=context.token
)
# 调用外部API执行机器人通用动作
if result.get("success", False):
# 获取任务ID
task_id = result.get("result", {}).get("id", "")
task_block_result = await wait_for_task_block_action_completion(task_id, context.token)
if task_block_result.get("success", False):
task_block_status = task_block_result.get("result", {}).get("status", "")
if task_block_status == 3:
result["message"] = f"机器人通用动作成功,目标站点: {target_site_label}"
elif task_block_status == 4:
result["message"] = f"机器人通用动作失败,目标站点: {target_site_label}:{task_block_result.get('message', '')}"
result["success"] = False
elif task_block_status == 5:
result["message"] = f"机器人通用动作终止,目标站点: {target_site_label}"
else:
result["message"] = f"机器人通用动作失败: {result.get('message', '未知错误')}"
# 记录执行结果
await self._record_task_log(block, result, context)
return result
except Exception as e:
result = {
"success": False,
"message": f"机器人通用动作执行异常: {str(e)}"
}
# 记录异常
await self._record_task_log(block, result, context)
return result
# 获取机器人位置处理器
@register_handler(RobotBlockName.VEHICLE_STATION)
class VehicleStationBlockHandler(RobotBlockHandler):
"""获取机器人位置处理器"""
async def execute(
self,
block: Dict[str, Any],
input_params: Dict[str, Any],
context: TaskContext
) -> Dict[str, Any]:
"""执行获取机器人位置操作"""
try:
# 获取关键参数用于验证
vehicle = input_params.get("vehicle")
# 参数检查
if not vehicle:
result = {
"success": False,
"message": "指定机器人不能为空"
}
await self._record_task_log(block, result, context)
return result
# 调用外部API获取机器人位置
result = await self._call_external_api("vehicle_station", input_params)
if result.get("success", False):
# 获取站点信息
station = result.get("data", {}).get("station", "")
last_station = result.get("data", {}).get("lastStation", "")
# 设置上下文变量
context.set_variable("station", station)
context.set_variable("lastStation", last_station)
context.set_block_output(block.get("name"), {
"station": station,
"lastStation": last_station
})
result["message"] = f"获取机器人 {vehicle} 位置成功,当前位置: {station}, 上次位置: {last_station}"
else:
result["message"] = f"获取机器人位置失败: {result.get('message', '未知错误')}"
# 记录执行结果
await self._record_task_log(block, result, context)
return result
except Exception as e:
result = {
"success": False,
"message": f"获取机器人位置执行异常: {str(e)}"
}
# 记录异常
await self._record_task_log(block, result, context)
return result
# 获取机器人电量处理器
@register_handler(RobotBlockName.GET_BATTERY_LEVEL)
class GetBatteryLevelBlockHandler(RobotBlockHandler):
"""获取机器人电量处理器"""
async def execute(
self,
block: Dict[str, Any],
input_params: Dict[str, Any],
context: TaskContext
) -> Dict[str, Any]:
"""执行获取机器人电量操作"""
try:
# 获取关键参数用于验证
vehicle = input_params.get("vehicle")
# 参数检查
if not vehicle:
result = {
"success": False,
"message": "机器人ID不能为空"
}
await self._record_task_log(block, result, context)
return result
# 调用外部API获取机器人电量
result = await self._call_external_api("get_battery_level", input_params)
if result.get("success", False):
# 获取电量信息
battery_level = result.get("data", {}).get("batteryLevel", 0.0)
# 设置上下文变量
context.set_variable("batteryLevel", battery_level)
context.set_block_output(block.get("name"), {"batteryLevel": battery_level})
# 格式化电量为百分比
battery_percent = f"{battery_level * 100:.1f}%"
result["message"] = f"获取机器人 {vehicle} 电量成功,当前电量: {battery_percent}"
else:
result["message"] = f"获取机器人电量失败: {result.get('message', '未知错误')}"
# 记录执行结果
await self._record_task_log(block, result, context)
return result
except Exception as e:
result = {
"success": False,
"message": f"获取机器人电量执行异常: {str(e)}"
}
# 记录异常
await self._record_task_log(block, result, context)
return result
# 获取机器人PGV码处理器
@register_handler(RobotBlockName.GET_PGV_CODE)
class GetPGVCodeBlockHandler(RobotBlockHandler):
"""获取机器人PGV码处理器"""
async def execute(
self,
block: Dict[str, Any],
input_params: Dict[str, Any],
context: TaskContext
) -> Dict[str, Any]:
"""执行获取机器人PGV码操作"""
try:
# 获取关键参数用于验证
vehicle = input_params.get("vehicle")
# 参数检查
if not vehicle:
result = {
"success": False,
"message": "机器人ID不能为空"
}
await self._record_task_log(block, result, context)
return result
# 调用外部API获取机器人PGV码
result = await self._call_external_api("get_pgv_code", input_params)
if result.get("success", False):
# 获取PGV码信息
code_info = result.get("data", {}).get("codeInfo", False)
# 设置上下文变量
context.set_variable("codeInfo", code_info)
context.set_block_output(block.get("name"), {"codeInfo": code_info})
code_status = "有效" if code_info else "无效"
result["message"] = f"获取机器人 {vehicle} PGV码成功二维码信息: {code_status}"
else:
result["message"] = f"获取机器人PGV码失败: {result.get('message', '未知错误')}"
# 记录执行结果
await self._record_task_log(block, result, context)
return result
except Exception as e:
result = {
"success": False,
"message": f"获取机器人PGV码执行异常: {str(e)}"
}
# 记录异常
await self._record_task_log(block, result, context)
return result
# 选择执行机器人处理器
@register_handler(RobotBlockName.SELECT_AGV)
class SelectAgvBlockHandler(RobotBlockHandler):
"""选择执行机器人处理器"""
async def execute(
self,
block: Dict[str, Any],
input_params: Dict[str, Any],
context: TaskContext
) -> Dict[str, Any]:
"""执行选择机器人操作"""
try:
# 获取关键参数用于验证
key_route = input_params.get("keyRoute")
priority = input_params.get("priority", 1)
if isinstance(priority, str):
priority = int(priority)
# 参数检查
if not key_route:
result = {
"success": False,
"message": "关键路径不能为空"
}
await self._record_task_log(block, result, context)
return result
# 调用外部API选择执行机器人
# result = await self._call_external_api("select_agv", input_params)
from services.sync_service import create_choose_amr_task, wait_for_amr_selection
result = await create_choose_amr_task(
task_id=context.task_record_id,
key_station_name=key_route,
amr_name=input_params.get("vehicle", ""),
amr_group_name=input_params.get("group", ""),
token=context.token,
priority=priority
)
if result.get("success", False):
# 获取任务块ID
task_block_id = result.get("result", {}).get("id", "")
if not task_block_id:
result = {
"success": False,
"message": "创建选择AMR任务成功但未返回任务块ID"
}
await self._record_task_log(block, result, context)
return result
logger.info(f"开始等待任务块 {task_block_id} 的AMR选择结果")
# 等待AMR选择完成
task_block_result = await wait_for_amr_selection(
task_block_id=task_block_id,
token=context.token
)
if not task_block_result or not task_block_result.get("success", False):
result = {
"success": False,
"message": f"等待AMR选择结果失败或超时任务块ID: {task_block_id}"
}
await self._record_task_log(block, result, context)
return result
# 获取选出的机器人ID
agv_id = task_block_result.get("result", {}).get("amrId", "")
if not agv_id:
result = {
"success": False,
"message": f"未能获取到选择的AMR ID任务块ID: {task_block_id}"
}
await self._record_task_log(block, result, context)
return result
# 获取当前块ID和名称
current_block_id = block.get("id", "unknown")
current_block_name = block.get("name", f"b{current_block_id}")
# 更新任务记录中的agv_id字段
await self._update_task_record_agv_id(context.task_record_id, agv_id)
logger.info(f"选择机器人块 {current_block_name}(ID:{current_block_id}) 选择的机器人: {agv_id}")
# 分析块的层级结构并记录关联关系
affected_blocks = self._analyze_affected_blocks(block, current_block_id, current_block_name)
# 将分析结果记录到日志
logger.info(f"选择机器人块 {current_block_name} 影响的块ID: {[b['id'] for b in affected_blocks]}")
context.set_block_output(current_block_name, {"selectedAgvId": agv_id})
# 为每个受影响的块设置变量记录它应该使用的机器人ID
for affected_block in affected_blocks:
affected_id = affected_block["id"]
affected_name = affected_block["name"]
context.set_variable(f"agv_for_block_{affected_id}", agv_id)
context.set_variable(f"agv_for_{affected_name}", agv_id)
context.set_variable(f"agv_task_id_{affected_id}", task_block_id)
context.set_block_output(affected_name, {"selectedAgvId": agv_id})
# 构造成功消息
vehicle = input_params.get("vehicle", "")
group = input_params.get("group", "")
tag = input_params.get("tag", "")
if vehicle:
result["message"] = f"指定机器人 {vehicle} 选择成功"
elif group:
result["message"] = f"从机器人组 {group} 选择机器人成功: {agv_id}"
elif tag:
result["message"] = f"根据标签 {tag} 选择机器人成功: {agv_id}"
else:
result["message"] = f"选择执行机器人成功: {agv_id}"
# 打印选择结果和影响的块
logger.info(f"选择机器人块 {current_block_name}(ID:{current_block_id}) 选择的机器人: {agv_id}")
logger.info(f"影响的块: {len(affected_blocks)}")
for i, b in enumerate(affected_blocks):
logger.info(f"{i+1}. {b['name']}(ID:{b['id']}, 类型:{b['type']}, 关系:{b['relation']})")
from services.execution.block_executor import BlockExecutor
executor = BlockExecutor(context)
# 检查是否有子块需要执行
has_children = "children" in block and "default" in block.get("children", {}) and len(block.get("children", {}).get("default", [])) > 0
if has_children:
# 执行子块
logger.info(f"开始执行选择机器人块 {current_block_name} 的子块")
loop_result = await executor.execute_children(block, "default")
# 处理子块执行结果
if loop_result.get("success", False):
# 子块执行成功,合并结果
logger.info(f"选择机器人块 {current_block_name} 的子块执行成功")
# 如果有需要合并的输出数据
child_results = loop_result.get("output", {}).get("results", [])
result["childBlockResults"] = child_results
# 保持成功状态
result["success"] = True
result["output"] = {
"selectedAgvId": agv_id,
"affectedBlocks": affected_blocks,
"childrenExecuted": True,
"childrenResult": loop_result
}
# 如果原始消息中没有包含子块执行信息,添加这部分信息
if "子块" not in result["message"]:
result["message"] = f"{result['message']},子块执行成功"
else:
# 子块执行失败,根据失败的子块更新消息
logger.error(f"选择机器人块 {current_block_name} 的子块执行失败: {loop_result.get('message')}")
# 创建包含子块失败信息的结果
error_msg = loop_result.get("message", "未知错误")
failed_block_id = loop_result.get("block_id", "unknown")
result = {
"success": False,
"message": f"选择执行机器人成功,但子块执行失败: {error_msg}失败块ID: {failed_block_id}",
"output": {
"selectedAgvId": agv_id,
"affectedBlocks": affected_blocks,
"childrenExecuted": False,
"childrenResult": loop_result
}
}
# 记录执行结果
from services.sync_service import closure_task
closure_result = await closure_task(
task_id=task_block_id,
token=context.token
)
else:
result["message"] = f"选择执行机器人失败: {result.get('message', '未知错误')}"
await self._record_task_log(block, result, context)
return result
except Exception as e:
result = {
"success": False,
"message": f"选择执行机器人异常: {str(e)}"
}
# 记录异常
await self._record_task_log(block, result, context)
return result