mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-04 13:25:13 +00:00
Workbench example, adjust log level, and ci check (#220) * TestLatency Return Value Example & gitignore update * Adjust log level & Add workbench virtual example & Add not action decorator & Add check_mode & * Add CI Check Fix/workstation yb revision (#217) * Revert log change & update registry * Revert opcua client & move electrolyte node Workstation yb merge dev ready 260113 (#216) * feat(bioyond): 添加计算实验设计功能,支持化合物配比和滴定比例参数 * feat(bioyond): 添加测量小瓶功能,支持基本参数配置 * feat(bioyond): 添加测量小瓶配置,支持新设备参数 * feat(bioyond): 更新仓库布局和尺寸,支持竖向排列的测量小瓶和试剂存放堆栈 * feat(bioyond): 优化任务创建流程,确保无论成功与否都清理任务队列以避免重复累积 * feat(bioyond): 添加设置反应器温度功能,支持温度范围和异常处理 * feat(bioyond): 调整反应器位置配置,统一坐标格式 * feat(bioyond): 添加调度器启动功能,支持任务队列执行并处理异常 * feat(bioyond): 优化调度器启动功能,添加异常处理并更新相关配置 * feat(opcua): 增强节点ID解析兼容性和数据类型处理 改进节点ID解析逻辑以支持多种格式,包括字符串和数字标识符 添加数据类型转换处理,确保写入值时类型匹配 优化错误提示信息,便于调试节点连接问题 * feat(registry): 新增后处理站的设备配置文件 添加后处理站的YAML配置文件,包含动作映射、状态类型和设备描述 * 添加调度器启动功能,合并物料参数配置,优化物料参数处理逻辑 * 添加从 Bioyond 系统自动同步工作流序列的功能,并更新相关配置 * fix:兼容 BioyondReactionStation 中 workflow_sequence 被重写为 property * fix:同步工作流序列 * feat: remove commented workflow synchronization from `reaction_station.py`. * 添加时间约束功能及相关配置 * fix:自动更新物料缓存功能,添加物料时更新缓存并在删除时移除缓存项 * fix:在添加物料时处理字符串和字典返回值,确保正确更新缓存 * fix:更新奔曜错误处理报送为物料变更报送,调整日志记录和响应消息 * feat:添加实验报告简化功能,去除冗余信息并保留关键信息 * feat: 添加任务状态事件发布功能,监控并报告任务运行、超时、完成和错误状态 * fix: 修复添加物料时数据格式错误 * Refactor bioyond_dispensing_station and reaction_station_bioyond YAML configurations - Removed redundant action value mappings from bioyond_dispensing_station. - Updated goal properties in bioyond_dispensing_station to use enums for target_stack and other parameters. - Changed data types for end_point and start_point in reaction_station_bioyond to use string enums (Start, End). - Simplified descriptions and updated measurement units from μL to mL where applicable. - Removed unused commands from reaction_station_bioyond to streamline the configuration. * fix:Change the material unit from μL to mL * fix:refresh_material_cache * feat: 动态获取工作流步骤ID,优化工作流配置 * feat: 添加清空服务端所有非核心工作流功能 * fix:修复Bottle类的序列化和反序列化方法 * feat:增强材料缓存更新逻辑,支持处理返回数据中的详细信息 * Add debug log * feat(workstation): update bioyond config migration and coin cell material search logic - Migrate bioyond_cell config to JSON structure and remove global variable dependencies - Implement material search confirmation dialog auto-handling - Add documentation: 20260113_物料搜寻确认弹窗自动处理功能.md and 20260113_配置迁移修改总结.md * Refactor module paths for Bioyond devices in YAML configuration files - Updated the module path for BioyondDispensingStation in bioyond_dispensing_station.yaml to reflect the new directory structure. - Updated the module path for BioyondReactionStation and BioyondReactor in reaction_station_bioyond.yaml to align with the revised organization of the codebase. * fix: WareHouse 的不可哈希类型错误,优化父节点去重逻辑 * refactor: Move config from module to instance initialization * fix: 修正 reaction_station 目录名拼写错误 * feat: Integrate material search logic and cleanup deprecated files - Update coin_cell_assembly.py with material search dialog handling - Update YB_warehouses.py with latest warehouse configurations - Remove outdated documentation and test data files * Refactor: Use instance attributes for action names and workflow step IDs * refactor: Split tipbox storage into left and right warehouses * refactor: Merge tipbox storage left and right into single warehouse --------- Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com> Co-authored-by: Andy6M <xieqiming1132@qq.com> fix: WareHouse 的不可哈希类型错误,优化父节点去重逻辑 fix parent_uuid fetch when bind_parent_id == node_name 物料更新也是用父节点进行报送 Add None conversion for tube rack etc. Add set_liquid example. Add create_resource and test_resource example. Add restart. Temp allow action message. Add no_update_feedback option. Create session_id by edge. bump version to 0.10.15 temp cancel update req
588 lines
21 KiB
Python
588 lines
21 KiB
Python
"""
|
||
Web API Controller
|
||
|
||
提供Web API的控制器函数,处理设备、任务和动作相关的业务逻辑
|
||
"""
|
||
|
||
import threading
|
||
import time
|
||
import traceback
|
||
import uuid
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional, Dict, Any, Tuple
|
||
|
||
from unilabos.app.model import JobAddReq, JobData
|
||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||
from unilabos.utils import logger
|
||
|
||
|
||
@dataclass
|
||
class JobResult:
|
||
"""任务结果数据"""
|
||
|
||
job_id: str
|
||
status: int # 4:SUCCEEDED, 5:CANCELED, 6:ABORTED
|
||
result: Dict[str, Any] = field(default_factory=dict)
|
||
feedback: Dict[str, Any] = field(default_factory=dict)
|
||
timestamp: float = field(default_factory=time.time)
|
||
|
||
|
||
class JobResultStore:
|
||
"""任务结果存储(单例)"""
|
||
|
||
_instance: Optional["JobResultStore"] = None
|
||
_lock = threading.Lock()
|
||
|
||
def __init__(self):
|
||
if not hasattr(self, "_initialized"):
|
||
self._results: Dict[str, JobResult] = {}
|
||
self._results_lock = threading.RLock()
|
||
self._initialized = True
|
||
|
||
def __new__(cls):
|
||
if cls._instance is None:
|
||
with cls._lock:
|
||
if cls._instance is None:
|
||
cls._instance = super().__new__(cls)
|
||
return cls._instance
|
||
|
||
def store_result(
|
||
self, job_id: str, status: int, result: Optional[Dict[str, Any]], feedback: Optional[Dict[str, Any]] = None
|
||
):
|
||
"""存储任务结果"""
|
||
with self._results_lock:
|
||
self._results[job_id] = JobResult(
|
||
job_id=job_id,
|
||
status=status,
|
||
result=result or {},
|
||
feedback=feedback or {},
|
||
timestamp=time.time(),
|
||
)
|
||
logger.trace(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
||
|
||
def get_and_remove(self, job_id: str) -> Optional[JobResult]:
|
||
"""获取并删除任务结果"""
|
||
with self._results_lock:
|
||
result = self._results.pop(job_id, None)
|
||
if result:
|
||
logger.trace(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
||
return result
|
||
|
||
def get_result(self, job_id: str) -> Optional[JobResult]:
|
||
"""仅获取任务结果(不删除)"""
|
||
with self._results_lock:
|
||
return self._results.get(job_id)
|
||
|
||
def cleanup_old_results(self, max_age_seconds: float = 3600):
|
||
"""清理过期的结果"""
|
||
current_time = time.time()
|
||
with self._results_lock:
|
||
expired_jobs = [
|
||
job_id for job_id, result in self._results.items() if current_time - result.timestamp > max_age_seconds
|
||
]
|
||
for job_id in expired_jobs:
|
||
del self._results[job_id]
|
||
logger.debug(f"[JobResultStore] Cleaned up expired result for job {job_id[:8]}")
|
||
|
||
|
||
# 全局结果存储实例
|
||
job_result_store = JobResultStore()
|
||
|
||
|
||
def store_job_result(
|
||
job_id: str, status: str, result: Optional[Dict[str, Any]], feedback: Optional[Dict[str, Any]] = None
|
||
):
|
||
"""存储任务结果(供外部调用)
|
||
|
||
Args:
|
||
job_id: 任务ID
|
||
status: 状态字符串 ("success", "failed", "cancelled")
|
||
result: 结果数据
|
||
feedback: 反馈数据
|
||
"""
|
||
# 转换状态字符串为整数
|
||
status_map = {
|
||
"success": 4, # SUCCEEDED
|
||
"failed": 6, # ABORTED
|
||
"cancelled": 5, # CANCELED
|
||
"running": 2, # EXECUTING
|
||
}
|
||
status_int = status_map.get(status, 0)
|
||
|
||
# 只存储最终状态
|
||
if status_int in (4, 5, 6):
|
||
job_result_store.store_result(job_id, status_int, result, feedback)
|
||
|
||
|
||
def get_resources() -> Tuple[bool, Any]:
|
||
"""获取资源配置
|
||
|
||
Returns:
|
||
Tuple[bool, Any]: (是否成功, 资源配置或错误信息)
|
||
"""
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
return False, "Host node not initialized"
|
||
|
||
return True, host_node.resources_config
|
||
|
||
|
||
def devices() -> Tuple[bool, Any]:
|
||
"""获取设备配置
|
||
|
||
Returns:
|
||
Tuple[bool, Any]: (是否成功, 设备配置或错误信息)
|
||
"""
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
return False, "Host node not initialized"
|
||
|
||
return True, host_node.devices_config
|
||
|
||
|
||
def job_info(job_id: str, remove_after_read: bool = True) -> JobData:
|
||
"""获取任务信息
|
||
|
||
Args:
|
||
job_id: 任务ID
|
||
remove_after_read: 是否在读取后删除结果(默认True)
|
||
|
||
Returns:
|
||
JobData: 任务数据
|
||
"""
|
||
# 首先检查结果存储中是否有已完成的结果
|
||
if remove_after_read:
|
||
stored_result = job_result_store.get_and_remove(job_id)
|
||
else:
|
||
stored_result = job_result_store.get_result(job_id)
|
||
|
||
if stored_result:
|
||
# 有存储的结果,直接返回
|
||
return JobData(
|
||
jobId=job_id,
|
||
status=stored_result.status,
|
||
result=stored_result.result,
|
||
)
|
||
|
||
# 没有存储的结果,从 HostNode 获取当前状态
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
return JobData(jobId=job_id, status=0)
|
||
|
||
get_goal_status = host_node.get_goal_status(job_id)
|
||
return JobData(jobId=job_id, status=get_goal_status)
|
||
|
||
|
||
def check_device_action_busy(device_id: str, action_name: str) -> Tuple[bool, Optional[str]]:
|
||
"""检查设备动作是否正在执行(被占用)
|
||
|
||
Args:
|
||
device_id: 设备ID
|
||
action_name: 动作名称
|
||
|
||
Returns:
|
||
Tuple[bool, Optional[str]]: (是否繁忙, 当前执行的job_id或None)
|
||
"""
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
return False, None
|
||
|
||
device_action_key = f"/devices/{device_id}/{action_name}"
|
||
|
||
# 检查 _device_action_status 中是否有正在执行的任务
|
||
if device_action_key in host_node._device_action_status:
|
||
status = host_node._device_action_status[device_action_key]
|
||
if status.job_ids:
|
||
# 返回第一个正在执行的job_id
|
||
current_job_id = next(iter(status.job_ids.keys()), None)
|
||
return True, current_job_id
|
||
|
||
return False, None
|
||
|
||
|
||
def _get_action_type(device_id: str, action_name: str) -> Optional[str]:
|
||
"""从注册表自动获取动作类型
|
||
|
||
Args:
|
||
device_id: 设备ID
|
||
action_name: 动作名称
|
||
|
||
Returns:
|
||
动作类型字符串,未找到返回None
|
||
"""
|
||
try:
|
||
from unilabos.ros.nodes.base_device_node import registered_devices
|
||
|
||
# 方法1: 从运行时注册设备获取
|
||
if device_id in registered_devices:
|
||
device_info = registered_devices[device_id]
|
||
base_node = device_info.get("base_node_instance")
|
||
if base_node and hasattr(base_node, "_action_value_mappings"):
|
||
action_mappings = base_node._action_value_mappings
|
||
# 尝试直接匹配或 auto- 前缀匹配
|
||
for key in [action_name, f"auto-{action_name}"]:
|
||
if key in action_mappings:
|
||
action_type = action_mappings[key].get("type")
|
||
if action_type:
|
||
# 转换为字符串格式
|
||
if hasattr(action_type, "__module__") and hasattr(action_type, "__name__"):
|
||
return f"{action_type.__module__}.{action_type.__name__}"
|
||
return str(action_type)
|
||
|
||
# 方法2: 从lab_registry获取
|
||
from unilabos.registry.registry import lab_registry
|
||
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node and lab_registry:
|
||
devices_config = host_node.devices_config
|
||
device_class = None
|
||
|
||
for tree in devices_config.trees:
|
||
node = tree.root_node
|
||
if node.res_content.id == device_id:
|
||
device_class = node.res_content.klass
|
||
break
|
||
|
||
if device_class and device_class in lab_registry.device_type_registry:
|
||
device_type_info = lab_registry.device_type_registry[device_class]
|
||
class_info = device_type_info.get("class", {})
|
||
action_mappings = class_info.get("action_value_mappings", {})
|
||
|
||
for key in [action_name, f"auto-{action_name}"]:
|
||
if key in action_mappings:
|
||
action_type = action_mappings[key].get("type")
|
||
if action_type:
|
||
if hasattr(action_type, "__module__") and hasattr(action_type, "__name__"):
|
||
return f"{action_type.__module__}.{action_type.__name__}"
|
||
return str(action_type)
|
||
|
||
except Exception as e:
|
||
logger.warning(f"[Controller] Failed to get action type for {device_id}/{action_name}: {str(e)}")
|
||
|
||
return None
|
||
|
||
|
||
def job_add(req: JobAddReq) -> JobData:
|
||
"""添加任务(检查设备是否繁忙,繁忙则返回失败)
|
||
|
||
Args:
|
||
req: 任务添加请求
|
||
|
||
Returns:
|
||
JobData: 任务数据(包含状态)
|
||
"""
|
||
# 服务端自动生成 job_id 和 task_id
|
||
job_id = str(uuid.uuid4())
|
||
task_id = str(uuid.uuid4())
|
||
|
||
# 服务端自动生成 server_info
|
||
server_info = {"send_timestamp": time.time()}
|
||
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
logger.error(f"[Controller] Host node not initialized for job: {job_id[:8]}")
|
||
return JobData(jobId=job_id, status=6) # 6 = ABORTED
|
||
|
||
# 解析动作信息
|
||
action_name = req.data.get("action", req.action) if req.data else req.action
|
||
action_args = req.data.get("action_kwargs") or req.data.get("action_args") if req.data else req.action_args
|
||
|
||
if action_args is None:
|
||
action_args = req.action_args or {}
|
||
elif isinstance(action_args, dict) and "command" in action_args:
|
||
action_args = action_args["command"]
|
||
|
||
# 自动获取 action_type
|
||
action_type = _get_action_type(req.device_id, action_name)
|
||
if action_type is None:
|
||
logger.error(f"[Controller] Action type not found for {req.device_id}/{action_name}")
|
||
return JobData(jobId=job_id, status=6) # ABORTED
|
||
|
||
# 检查设备动作是否繁忙
|
||
is_busy, current_job_id = check_device_action_busy(req.device_id, action_name)
|
||
|
||
if is_busy:
|
||
logger.warning(
|
||
f"[Controller] Device action busy: {req.device_id}/{action_name}, "
|
||
f"current job: {current_job_id[:8] if current_job_id else 'unknown'}"
|
||
)
|
||
# 返回失败状态,status=6 表示 ABORTED
|
||
return JobData(jobId=job_id, status=6)
|
||
|
||
# 设备空闲,提交任务执行
|
||
try:
|
||
from unilabos.app.ws_client import QueueItem
|
||
|
||
device_action_key = f"/devices/{req.device_id}/{action_name}"
|
||
queue_item = QueueItem(
|
||
task_type="job_call_back_status",
|
||
device_id=req.device_id,
|
||
action_name=action_name,
|
||
task_id=task_id,
|
||
job_id=job_id,
|
||
device_action_key=device_action_key,
|
||
)
|
||
|
||
host_node.send_goal(
|
||
queue_item,
|
||
action_type=action_type,
|
||
action_kwargs=action_args,
|
||
server_info=server_info,
|
||
)
|
||
|
||
logger.info(f"[Controller] Job submitted: {job_id[:8]} -> {req.device_id}/{action_name}")
|
||
# 返回已接受状态,status=1 表示 ACCEPTED
|
||
return JobData(jobId=job_id, status=1)
|
||
|
||
except ValueError as e:
|
||
# ActionClient not found 等错误
|
||
logger.error(f"[Controller] Action not available: {str(e)}")
|
||
return JobData(jobId=job_id, status=6) # ABORTED
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Controller] Error submitting job: {str(e)}")
|
||
traceback.print_exc()
|
||
return JobData(jobId=job_id, status=6) # ABORTED
|
||
|
||
|
||
def get_online_devices() -> Tuple[bool, Dict[str, Any]]:
|
||
"""获取在线设备列表
|
||
|
||
Returns:
|
||
Tuple[bool, Dict]: (是否成功, 在线设备信息)
|
||
"""
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
return False, {"error": "Host node not initialized"}
|
||
|
||
try:
|
||
from unilabos.ros.nodes.base_device_node import registered_devices
|
||
|
||
online_devices = {}
|
||
for device_key in host_node._online_devices:
|
||
# device_key 格式: "namespace/device_id"
|
||
parts = device_key.split("/")
|
||
if len(parts) >= 2:
|
||
device_id = parts[-1]
|
||
else:
|
||
device_id = device_key
|
||
|
||
# 获取设备详细信息
|
||
device_info = registered_devices.get(device_id, {})
|
||
machine_name = host_node.device_machine_names.get(device_id, "未知")
|
||
|
||
online_devices[device_id] = {
|
||
"device_key": device_key,
|
||
"namespace": host_node.devices_names.get(device_id, ""),
|
||
"machine_name": machine_name,
|
||
"uuid": device_info.get("uuid", "") if device_info else "",
|
||
"node_name": device_info.get("node_name", "") if device_info else "",
|
||
}
|
||
|
||
return True, {
|
||
"online_devices": online_devices,
|
||
"total_count": len(online_devices),
|
||
"timestamp": time.time(),
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Controller] Error getting online devices: {str(e)}")
|
||
traceback.print_exc()
|
||
return False, {"error": str(e)}
|
||
|
||
|
||
def get_device_actions(device_id: str) -> Tuple[bool, Dict[str, Any]]:
|
||
"""获取设备可用的动作列表
|
||
|
||
Args:
|
||
device_id: 设备ID
|
||
|
||
Returns:
|
||
Tuple[bool, Dict]: (是否成功, 动作列表信息)
|
||
"""
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
return False, {"error": "Host node not initialized"}
|
||
|
||
try:
|
||
from unilabos.ros.nodes.base_device_node import registered_devices
|
||
from unilabos.app.web.utils.action_utils import get_action_info
|
||
|
||
# 检查设备是否已注册
|
||
if device_id not in registered_devices:
|
||
return False, {"error": f"Device not found: {device_id}"}
|
||
|
||
device_info = registered_devices[device_id]
|
||
actions = device_info.get("actions", {})
|
||
|
||
actions_list = {}
|
||
for action_name, action_server in actions.items():
|
||
try:
|
||
action_info = get_action_info(action_server, action_name)
|
||
# 检查动作是否繁忙
|
||
is_busy, current_job = check_device_action_busy(device_id, action_name)
|
||
actions_list[action_name] = {
|
||
**action_info,
|
||
"is_busy": is_busy,
|
||
"current_job_id": current_job[:8] if current_job else None,
|
||
}
|
||
except Exception as e:
|
||
logger.warning(f"[Controller] Error getting action info for {action_name}: {str(e)}")
|
||
actions_list[action_name] = {
|
||
"type_name": "unknown",
|
||
"action_path": f"/devices/{device_id}/{action_name}",
|
||
"is_busy": False,
|
||
"error": str(e),
|
||
}
|
||
|
||
return True, {
|
||
"device_id": device_id,
|
||
"actions": actions_list,
|
||
"action_count": len(actions_list),
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Controller] Error getting device actions: {str(e)}")
|
||
traceback.print_exc()
|
||
return False, {"error": str(e)}
|
||
|
||
|
||
def get_action_schema(device_id: str, action_name: str) -> Tuple[bool, Dict[str, Any]]:
|
||
"""获取动作的Schema详情
|
||
|
||
Args:
|
||
device_id: 设备ID
|
||
action_name: 动作名称
|
||
|
||
Returns:
|
||
Tuple[bool, Dict]: (是否成功, Schema信息)
|
||
"""
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
return False, {"error": "Host node not initialized"}
|
||
|
||
try:
|
||
from unilabos.registry.registry import lab_registry
|
||
from unilabos.ros.nodes.base_device_node import registered_devices
|
||
|
||
result = {
|
||
"device_id": device_id,
|
||
"action_name": action_name,
|
||
"schema": None,
|
||
"goal_default": None,
|
||
"action_type": None,
|
||
"is_busy": False,
|
||
}
|
||
|
||
# 检查动作是否繁忙
|
||
is_busy, current_job = check_device_action_busy(device_id, action_name)
|
||
result["is_busy"] = is_busy
|
||
result["current_job_id"] = current_job[:8] if current_job else None
|
||
|
||
# 方法1: 从 registered_devices 获取运行时信息
|
||
if device_id in registered_devices:
|
||
device_info = registered_devices[device_id]
|
||
base_node = device_info.get("base_node_instance")
|
||
|
||
if base_node and hasattr(base_node, "_action_value_mappings"):
|
||
action_mappings = base_node._action_value_mappings
|
||
if action_name in action_mappings:
|
||
mapping = action_mappings[action_name]
|
||
result["schema"] = mapping.get("schema")
|
||
result["goal_default"] = mapping.get("goal_default")
|
||
result["action_type"] = str(mapping.get("type", ""))
|
||
|
||
# 方法2: 从 lab_registry 获取注册表信息(如果运行时没有)
|
||
if result["schema"] is None and lab_registry:
|
||
# 尝试查找设备类型
|
||
devices_config = host_node.devices_config
|
||
device_class = None
|
||
|
||
# 从配置中获取设备类型
|
||
for tree in devices_config.trees:
|
||
node = tree.root_node
|
||
if node.res_content.id == device_id:
|
||
device_class = node.res_content.klass
|
||
break
|
||
|
||
if device_class and device_class in lab_registry.device_type_registry:
|
||
device_type_info = lab_registry.device_type_registry[device_class]
|
||
class_info = device_type_info.get("class", {})
|
||
action_mappings = class_info.get("action_value_mappings", {})
|
||
|
||
# 尝试直接匹配或 auto- 前缀匹配
|
||
for key in [action_name, f"auto-{action_name}"]:
|
||
if key in action_mappings:
|
||
mapping = action_mappings[key]
|
||
result["schema"] = mapping.get("schema")
|
||
result["goal_default"] = mapping.get("goal_default")
|
||
result["action_type"] = str(mapping.get("type", ""))
|
||
result["handles"] = mapping.get("handles", {})
|
||
result["placeholder_keys"] = mapping.get("placeholder_keys", {})
|
||
break
|
||
|
||
if result["schema"] is None:
|
||
return False, {"error": f"Action schema not found: {device_id}/{action_name}"}
|
||
|
||
return True, result
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Controller] Error getting action schema: {str(e)}")
|
||
traceback.print_exc()
|
||
return False, {"error": str(e)}
|
||
|
||
|
||
def get_all_available_actions() -> Tuple[bool, Dict[str, Any]]:
|
||
"""获取所有设备的可用动作
|
||
|
||
Returns:
|
||
Tuple[bool, Dict]: (是否成功, 所有设备的动作信息)
|
||
"""
|
||
host_node = HostNode.get_instance(0)
|
||
if host_node is None:
|
||
return False, {"error": "Host node not initialized"}
|
||
|
||
try:
|
||
from unilabos.ros.nodes.base_device_node import registered_devices
|
||
from unilabos.app.web.utils.action_utils import get_action_info
|
||
|
||
all_actions = {}
|
||
total_action_count = 0
|
||
|
||
for device_id, device_info in registered_devices.items():
|
||
actions = device_info.get("actions", {})
|
||
device_actions = {}
|
||
|
||
for action_name, action_server in actions.items():
|
||
try:
|
||
action_info = get_action_info(action_server, action_name)
|
||
is_busy, current_job = check_device_action_busy(device_id, action_name)
|
||
device_actions[action_name] = {
|
||
"type_name": action_info.get("type_name", ""),
|
||
"action_path": action_info.get("action_path", ""),
|
||
"is_busy": is_busy,
|
||
"current_job_id": current_job[:8] if current_job else None,
|
||
}
|
||
total_action_count += 1
|
||
except Exception as e:
|
||
logger.warning(f"[Controller] Error processing action {device_id}/{action_name}: {str(e)}")
|
||
|
||
if device_actions:
|
||
all_actions[device_id] = {
|
||
"actions": device_actions,
|
||
"action_count": len(device_actions),
|
||
"machine_name": host_node.device_machine_names.get(device_id, "未知"),
|
||
}
|
||
|
||
return True, {
|
||
"devices": all_actions,
|
||
"device_count": len(all_actions),
|
||
"total_action_count": total_action_count,
|
||
"timestamp": time.time(),
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Controller] Error getting all available actions: {str(e)}")
|
||
traceback.print_exc()
|
||
return False, {"error": str(e)}
|