mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-05 22:15:04 +00:00
Upgrade to py 3.11.14; ROS2 Humble 0.7; unilabos 0.10.16
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
This commit is contained in:
@@ -20,6 +20,8 @@ from rclpy.callback_groups import ReentrantCallbackGroup
|
||||
from rclpy.service import Service
|
||||
from unilabos_msgs.action import SendCmd
|
||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.utils.decorator import get_topic_config, get_all_subscriptions
|
||||
|
||||
from unilabos.resources.container import RegularContainer
|
||||
@@ -47,7 +49,6 @@ from unilabos.resources.resource_tracker import (
|
||||
ResourceTreeInstance,
|
||||
ResourceDictInstance,
|
||||
)
|
||||
from unilabos.ros.x.rclpyx import get_event_loop
|
||||
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
||||
from rclpy.task import Task, Future
|
||||
from unilabos.utils.import_manager import default_manager
|
||||
@@ -183,7 +184,7 @@ class PropertyPublisher:
|
||||
f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}"
|
||||
)
|
||||
self.timer = node.create_timer(self.timer_period, self.publish_property)
|
||||
self.__loop = get_event_loop()
|
||||
self.__loop = ROS2DeviceNode.get_asyncio_loop()
|
||||
str_msg_type = str(msg_type)[8:-2]
|
||||
self.node.lab_logger().trace(f"发布属性: {name}, 类型: {str_msg_type}, 周期: {initial_period}秒, QoS: {qos}")
|
||||
|
||||
@@ -390,9 +391,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
parent_resource = self.resource_tracker.figure_resource(
|
||||
{"name": bind_parent_id}
|
||||
)
|
||||
for r in rts.root_nodes:
|
||||
# noinspection PyUnresolvedReferences
|
||||
r.res_content.parent_uuid = parent_resource.unilabos_uuid
|
||||
for r in rts.root_nodes:
|
||||
# noinspection PyUnresolvedReferences
|
||||
r.res_content.parent_uuid = parent_resource.unilabos_uuid
|
||||
else:
|
||||
for r in rts.root_nodes:
|
||||
r.res_content.parent_uuid = self.uuid
|
||||
|
||||
if len(LIQUID_INPUT_SLOT) and LIQUID_INPUT_SLOT[0] == -1 and len(rts.root_nodes) == 1 and isinstance(rts.root_nodes[0], RegularContainer):
|
||||
# noinspection PyTypeChecker
|
||||
@@ -428,11 +432,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
})
|
||||
tree_response: SerialCommand.Response = await client.call_async(request)
|
||||
uuid_maps = json.loads(tree_response.response)
|
||||
self.resource_tracker.loop_update_uuid(input_resources, uuid_maps)
|
||||
plr_instances = rts.to_plr_resources()
|
||||
for plr_instance in plr_instances:
|
||||
self.resource_tracker.loop_update_uuid(plr_instance, uuid_maps)
|
||||
rts: ResourceTreeSet = ResourceTreeSet.from_plr_resources(plr_instances)
|
||||
self.lab_logger().info(f"Resource tree added. UUID mapping: {len(uuid_maps)} nodes")
|
||||
final_response = {
|
||||
"created_resources": rts.dump(),
|
||||
"liquid_input_resources": [],
|
||||
"created_resource_tree": rts.dump(),
|
||||
"liquid_input_resource_tree": [],
|
||||
}
|
||||
res.response = json.dumps(final_response)
|
||||
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
||||
@@ -458,7 +465,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
return res
|
||||
try:
|
||||
if len(rts.root_nodes) == 1 and parent_resource is not None:
|
||||
plr_instance = rts.to_plr_resources()[0]
|
||||
plr_instance = plr_instances[0]
|
||||
if isinstance(plr_instance, Plate):
|
||||
empty_liquid_info_in: List[Tuple[Optional[str], float]] = [(None, 0)] * plr_instance.num_items
|
||||
if len(ADD_LIQUID_TYPE) == 1 and len(LIQUID_VOLUME) == 1 and len(LIQUID_INPUT_SLOT) > 1:
|
||||
@@ -483,7 +490,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
input_wells = []
|
||||
for r in LIQUID_INPUT_SLOT:
|
||||
input_wells.append(plr_instance.children[r])
|
||||
final_response["liquid_input_resources"] = ResourceTreeSet.from_plr_resources(input_wells).dump()
|
||||
final_response["liquid_input_resource_tree"] = ResourceTreeSet.from_plr_resources(input_wells).dump()
|
||||
res.response = json.dumps(final_response)
|
||||
if issubclass(parent_resource.__class__, Deck) and hasattr(parent_resource, "assign_child_at_slot") and "slot" in other_calling_param:
|
||||
other_calling_param["slot"] = int(other_calling_param["slot"])
|
||||
@@ -617,7 +624,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
) # type: ignore
|
||||
raw_nodes = json.loads(response.response)
|
||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_nodes)
|
||||
self.lab_logger().debug(f"获取资源结果: {len(tree_set.trees)} 个资源树")
|
||||
self.lab_logger().trace(f"获取资源结果: {len(tree_set.trees)} 个资源树 {tree_set.root_nodes}")
|
||||
return tree_set
|
||||
|
||||
async def get_resource_with_dir(self, resource_id: str, with_children: bool = True) -> "ResourcePLR":
|
||||
@@ -651,61 +658,71 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
def transfer_to_new_resource(
|
||||
self, plr_resource: "ResourcePLR", tree: ResourceTreeInstance, additional_add_params: Dict[str, Any]
|
||||
):
|
||||
) -> Optional["ResourcePLR"]:
|
||||
parent_uuid = tree.root_node.res_content.parent_uuid
|
||||
if parent_uuid:
|
||||
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
|
||||
if parent_resource is None:
|
||||
if not parent_uuid:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource} parent未知,挂载到当前节点下,额外参数:{additional_add_params}"
|
||||
)
|
||||
return None
|
||||
if parent_uuid == self.uuid:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载到{self.identifier},额外参数:{additional_add_params}"
|
||||
)
|
||||
return None
|
||||
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
|
||||
if parent_resource is None:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
# 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步
|
||||
additional_params = {}
|
||||
extra = getattr(plr_resource, "unilabos_extra", {})
|
||||
if len(extra):
|
||||
self.lab_logger().info(f"发现物料{plr_resource}额外参数: " + str(extra))
|
||||
if "update_resource_site" in extra:
|
||||
additional_add_params["site"] = extra["update_resource_site"]
|
||||
site = additional_add_params.get("site", None)
|
||||
spec = inspect.signature(parent_resource.assign_child_resource)
|
||||
if "spot" in spec.parameters:
|
||||
ordering_dict: Dict[str, Any] = getattr(parent_resource, "_ordering")
|
||||
if ordering_dict:
|
||||
site = list(ordering_dict.keys()).index(site)
|
||||
additional_params["spot"] = site
|
||||
old_parent = plr_resource.parent
|
||||
if old_parent is not None:
|
||||
# plr并不支持同一个deck的加载和卸载
|
||||
self.lab_logger().warning(f"物料{plr_resource}请求从{old_parent}卸载")
|
||||
old_parent.unassign_child_resource(plr_resource)
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在"
|
||||
f"物料{plr_resource}请求挂载到{parent_resource},额外参数:{additional_params}"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
# 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步
|
||||
additional_params = {}
|
||||
extra = getattr(plr_resource, "unilabos_extra", {})
|
||||
if len(extra):
|
||||
self.lab_logger().info(f"发现物料{plr_resource}额外参数: " + str(extra))
|
||||
if "update_resource_site" in extra:
|
||||
additional_add_params["site"] = extra["update_resource_site"]
|
||||
site = additional_add_params.get("site", None)
|
||||
spec = inspect.signature(parent_resource.assign_child_resource)
|
||||
if "spot" in spec.parameters:
|
||||
ordering_dict: Dict[str, Any] = getattr(parent_resource, "_ordering")
|
||||
if ordering_dict:
|
||||
site = list(ordering_dict.keys()).index(site)
|
||||
additional_params["spot"] = site
|
||||
old_parent = plr_resource.parent
|
||||
if old_parent is not None:
|
||||
# plr并不支持同一个deck的加载和卸载
|
||||
self.lab_logger().warning(f"物料{plr_resource}请求从{old_parent}卸载")
|
||||
old_parent.unassign_child_resource(plr_resource)
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载到{parent_resource},额外参数:{additional_params}"
|
||||
)
|
||||
|
||||
# ⭐ assign 之前,需要从 resources 列表中移除
|
||||
# 因为资源将不再是顶级资源,而是成为 parent_resource 的子资源
|
||||
# 如果不移除,figure_resource 会找到两次:一次在 resources,一次在 parent 的 children
|
||||
resource_id = id(plr_resource)
|
||||
for i, r in enumerate(self.resource_tracker.resources):
|
||||
if id(r) == resource_id:
|
||||
self.resource_tracker.resources.pop(i)
|
||||
self.lab_logger().debug(
|
||||
f"从顶级资源列表中移除 {plr_resource.name}(即将成为 {parent_resource.name} 的子资源)"
|
||||
)
|
||||
break
|
||||
# ⭐ assign 之前,需要从 resources 列表中移除
|
||||
# 因为资源将不再是顶级资源,而是成为 parent_resource 的子资源
|
||||
# 如果不移除,figure_resource 会找到两次:一次在 resources,一次在 parent 的 children
|
||||
resource_id = id(plr_resource)
|
||||
for i, r in enumerate(self.resource_tracker.resources):
|
||||
if id(r) == resource_id:
|
||||
self.resource_tracker.resources.pop(i)
|
||||
self.lab_logger().debug(
|
||||
f"从顶级资源列表中移除 {plr_resource.name}(即将成为 {parent_resource.name} 的子资源)"
|
||||
)
|
||||
break
|
||||
|
||||
parent_resource.assign_child_resource(plr_resource, location=None, **additional_params)
|
||||
parent_resource.assign_child_resource(plr_resource, location=None, **additional_params)
|
||||
|
||||
func = getattr(self.driver_instance, "resource_tree_transfer", None)
|
||||
if callable(func):
|
||||
# 分别是 物料的原来父节点,当前物料的状态,物料的新父节点(此时物料已经重新assign了)
|
||||
func(old_parent, plr_resource, parent_resource)
|
||||
except Exception as e:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}"
|
||||
)
|
||||
func = getattr(self.driver_instance, "resource_tree_transfer", None)
|
||||
if callable(func):
|
||||
# 分别是 物料的原来父节点,当前物料的状态,物料的新父节点(此时物料已经重新assign了)
|
||||
func(old_parent, plr_resource, parent_resource)
|
||||
return parent_resource
|
||||
except Exception as e:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
"""
|
||||
@@ -720,7 +737,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
def _handle_add(
|
||||
plr_resources: List[ResourcePLR], tree_set: ResourceTreeSet, additional_add_params: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
) -> Tuple[Dict[str, Any], List[ResourcePLR]]:
|
||||
"""
|
||||
处理资源添加操作的内部函数
|
||||
|
||||
@@ -732,15 +749,20 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
Returns:
|
||||
操作结果字典
|
||||
"""
|
||||
parents = [] # 放的是被变更的物料 / 被变更的物料父级
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
self.resource_tracker.add_resource(plr_resource)
|
||||
self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
|
||||
parent = self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
|
||||
if parent is not None:
|
||||
parents.append(parent)
|
||||
else:
|
||||
parents.append(plr_resource)
|
||||
|
||||
func = getattr(self.driver_instance, "resource_tree_add", None)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
|
||||
return {"success": True, "action": "add"}
|
||||
return {"success": True, "action": "add"}, parents
|
||||
|
||||
def _handle_remove(resources_uuid: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -775,11 +797,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if plr_resource.parent is not None:
|
||||
plr_resource.parent.unassign_child_resource(plr_resource)
|
||||
self.resource_tracker.remove_resource(plr_resource)
|
||||
self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点")
|
||||
self.lab_logger().info(f"[资源同步] 移除物料 {plr_resource} 及其子节点")
|
||||
|
||||
for other_plr_resource in other_plr_resources:
|
||||
self.resource_tracker.remove_resource(other_plr_resource)
|
||||
self.lab_logger().info(f"移除物料 {other_plr_resource} 及其子节点")
|
||||
self.lab_logger().info(f"[资源同步] 移除物料 {other_plr_resource} 及其子节点")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -790,7 +812,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
def _handle_update(
|
||||
plr_resources: List[Union[ResourcePLR, ResourceDictInstance]], tree_set: ResourceTreeSet, additional_add_params: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
) -> Tuple[Dict[str, Any], List[ResourcePLR]]:
|
||||
"""
|
||||
处理资源更新操作的内部函数
|
||||
|
||||
@@ -802,6 +824,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
Returns:
|
||||
操作结果字典
|
||||
"""
|
||||
original_instances = []
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
if isinstance(plr_resource, ResourceDictInstance):
|
||||
self._lab_logger.info(f"跳过 非资源{plr_resource.res_content.name} 的更新")
|
||||
@@ -810,11 +833,16 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
|
||||
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
|
||||
)
|
||||
original_parent_resource = original_instance.parent
|
||||
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
|
||||
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
|
||||
not_same_parent = original_parent_resource_uuid != target_parent_resource_uuid and original_parent_resource is not None
|
||||
old_name = original_instance.name
|
||||
new_name = plr_resource.name
|
||||
parent_appended = False
|
||||
|
||||
# Update操作中包含改名:需要先remove再add
|
||||
if original_instance.name != plr_resource.name:
|
||||
old_name = original_instance.name
|
||||
new_name = plr_resource.name
|
||||
# Update操作中包含改名:需要先remove再add,这里更新父节点即可
|
||||
if not not_same_parent and old_name != new_name:
|
||||
self.lab_logger().info(f"物料改名操作:{old_name} -> {new_name}")
|
||||
|
||||
# 收集所有相关的uuid(包括子节点)
|
||||
@@ -823,12 +851,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
_handle_add([original_instance], tree_set, additional_add_params)
|
||||
|
||||
self.lab_logger().info(f"物料改名完成:{old_name} -> {new_name}")
|
||||
original_instances.append(original_parent_resource)
|
||||
parent_appended = True
|
||||
|
||||
# 常规更新:不涉及改名
|
||||
original_parent_resource = original_instance.parent
|
||||
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
|
||||
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
|
||||
|
||||
self.lab_logger().info(
|
||||
f"物料{original_instance} 原始父节点{original_parent_resource_uuid} "
|
||||
f"目标父节点{target_parent_resource_uuid} 更新"
|
||||
@@ -839,13 +865,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
original_instance.unilabos_extra = getattr(plr_resource, "unilabos_extra") # type: ignore # noqa: E501
|
||||
|
||||
# 如果父节点变化,需要重新挂载
|
||||
if (
|
||||
original_parent_resource_uuid != target_parent_resource_uuid
|
||||
and original_parent_resource is not None
|
||||
):
|
||||
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
if not_same_parent:
|
||||
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
original_instances.append(parent)
|
||||
parent_appended = True
|
||||
else:
|
||||
# 判断是否变更了resource_site
|
||||
# 判断是否变更了resource_site,重新登记
|
||||
target_site = original_instance.unilabos_extra.get("update_resource_site")
|
||||
sites = original_instance.parent.sites if original_instance.parent is not None and hasattr(original_instance.parent, "sites") else None
|
||||
site_names = list(original_instance.parent._ordering.keys()) if original_instance.parent is not None and hasattr(original_instance.parent, "sites") else []
|
||||
@@ -853,7 +878,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
site_index = sites.index(original_instance)
|
||||
site_name = site_names[site_index]
|
||||
if site_name != target_site:
|
||||
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
if parent is not None:
|
||||
original_instances.append(parent)
|
||||
parent_appended = True
|
||||
|
||||
# 加载状态
|
||||
original_instance.load_all_state(states)
|
||||
@@ -861,13 +889,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
self.lab_logger().info(
|
||||
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] " f"及其子节点 {child_count} 个"
|
||||
)
|
||||
if not parent_appended:
|
||||
original_instances.append(original_instance)
|
||||
|
||||
# 调用driver的update回调
|
||||
func = getattr(self.driver_instance, "resource_tree_update", None)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
func(original_instances)
|
||||
|
||||
return {"success": True, "action": "update"}
|
||||
return {"success": True, "action": "update"}, original_instances
|
||||
|
||||
try:
|
||||
data = json.loads(req.command)
|
||||
@@ -877,8 +907,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
action = i.get("action") # remove, add, update
|
||||
resources_uuid: List[str] = i.get("data") # 资源数据
|
||||
additional_add_params = i.get("additional_add_params", {}) # 额外参数
|
||||
self.lab_logger().info(
|
||||
f"[Resource Tree Update] Processing {action} operation, " f"resources count: {len(resources_uuid)}"
|
||||
self.lab_logger().trace(
|
||||
f"[资源同步] 处理 {action}, " f"resources count: {len(resources_uuid)}"
|
||||
)
|
||||
tree_set = None
|
||||
if action in ["add", "update"]:
|
||||
@@ -890,8 +920,20 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if tree_set is None:
|
||||
raise ValueError("tree_set不能为None")
|
||||
plr_resources = tree_set.to_plr_resources()
|
||||
result = _handle_add(plr_resources, tree_set, additional_add_params)
|
||||
new_tree_set = ResourceTreeSet.from_plr_resources(plr_resources)
|
||||
result, parents = _handle_add(plr_resources, tree_set, additional_add_params)
|
||||
parents: List[Optional["ResourcePLR"]] = [i for i in parents if i is not None]
|
||||
# de_dupe_parents = list(set(parents))
|
||||
# Fix unhashable type error for WareHouse
|
||||
de_dupe_parents = []
|
||||
_seen_ids = set()
|
||||
for p in parents:
|
||||
if id(p) not in _seen_ids:
|
||||
_seen_ids.add(id(p))
|
||||
de_dupe_parents.append(p)
|
||||
new_tree_set = ResourceTreeSet.from_plr_resources(de_dupe_parents) # 去重
|
||||
for tree in new_tree_set.trees:
|
||||
if tree.root_node.res_content.uuid_parent is None and self.node_name != "host_node":
|
||||
tree.root_node.res_content.parent_uuid = self.uuid
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps(
|
||||
{"data": {"data": new_tree_set.dump()}, "action": "update"}) # 和Update Resource一致
|
||||
@@ -908,14 +950,18 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
plr_resources.append(tree.root_node)
|
||||
else:
|
||||
plr_resources.append(ResourceTreeSet([tree]).to_plr_resources()[0])
|
||||
new_tree_set = ResourceTreeSet.from_plr_resources(plr_resources)
|
||||
result = _handle_update(plr_resources, tree_set, additional_add_params)
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps(
|
||||
{"data": {"data": new_tree_set.dump()}, "action": "update"}) # 和Update Resource一致
|
||||
response: SerialCommand_Response = await self._resource_clients[
|
||||
"c2s_update_resource_tree"].call_async(r) # type: ignore
|
||||
self.lab_logger().info(f"确认资源云端 Update 结果: {response.response}")
|
||||
result, original_instances = _handle_update(plr_resources, tree_set, additional_add_params)
|
||||
if not BasicConfig.no_update_feedback:
|
||||
new_tree_set = ResourceTreeSet.from_plr_resources(original_instances) # 去重
|
||||
for tree in new_tree_set.trees:
|
||||
if tree.root_node.res_content.uuid_parent is None and self.node_name != "host_node":
|
||||
tree.root_node.res_content.parent_uuid = self.uuid
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps(
|
||||
{"data": {"data": new_tree_set.dump()}, "action": "update"}) # 和Update Resource一致
|
||||
response: SerialCommand_Response = await self._resource_clients[
|
||||
"c2s_update_resource_tree"].call_async(r) # type: ignore
|
||||
self.lab_logger().info(f"确认资源云端 Update 结果: {response.response}")
|
||||
results.append(result)
|
||||
elif action == "remove":
|
||||
result = _handle_remove(resources_uuid)
|
||||
@@ -929,15 +975,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# 返回处理结果
|
||||
result_json = {"results": results, "total": len(data)}
|
||||
res.response = json.dumps(result_json, ensure_ascii=False, cls=TypeEncoder)
|
||||
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
|
||||
# self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
error_msg = f"Invalid JSON format: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
self.lab_logger().error(f"[资源同步] {error_msg}")
|
||||
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
self.lab_logger().error(f"[资源同步] {error_msg}")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
|
||||
|
||||
@@ -1258,7 +1304,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
|
||||
|
||||
action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
|
||||
self.lab_logger().debug(f"任务 {ACTION.__name__} 接收到原始目标: {action_kwargs}")
|
||||
self.lab_logger().debug(f"任务 {ACTION.__name__} 接收到原始目标: {str(action_kwargs)[:1000]}")
|
||||
self.lab_logger().trace(f"任务 {ACTION.__name__} 接收到原始目标: {action_kwargs}")
|
||||
error_skip = False
|
||||
# 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过
|
||||
if action_name not in ["create_resource_detailed", "create_resource"]:
|
||||
@@ -1274,9 +1321,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# 批量查询资源
|
||||
queried_resources = []
|
||||
for resource_data in resource_inputs:
|
||||
plr_resource = await self.get_resource_with_dir(
|
||||
resource_id=resource_data["id"], with_children=True
|
||||
)
|
||||
unilabos_uuid = resource_data.get("data", {}).get("unilabos_uuid")
|
||||
if unilabos_uuid is None:
|
||||
plr_resource = await self.get_resource_with_dir(
|
||||
resource_id=resource_data["id"], with_children=True
|
||||
)
|
||||
else:
|
||||
resource_tree = await self.get_resource([unilabos_uuid])
|
||||
plr_resource = resource_tree.to_plr_resources()[0]
|
||||
if "sample_id" in resource_data:
|
||||
plr_resource.unilabos_extra["sample_uuid"] = resource_data["sample_id"]
|
||||
queried_resources.append(plr_resource)
|
||||
@@ -1325,9 +1377,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
execution_success = True
|
||||
except Exception as _:
|
||||
execution_error = traceback.format_exc()
|
||||
error(
|
||||
f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}"
|
||||
)
|
||||
error(f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{str(action_kwargs)[:1000]}")
|
||||
trace(f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}")
|
||||
|
||||
future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs)
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
@@ -1347,8 +1398,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
except Exception as _:
|
||||
execution_error = traceback.format_exc()
|
||||
error(
|
||||
f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}"
|
||||
)
|
||||
f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{str(action_kwargs)[:1000]}")
|
||||
trace(
|
||||
f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}")
|
||||
|
||||
future.add_done_callback(_handle_future_exception)
|
||||
|
||||
@@ -1416,7 +1468,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
for r in rs:
|
||||
res = self.resource_tracker.parent_resource(r) # 获取 resource 对象
|
||||
else:
|
||||
res = self.resource_tracker.parent_resource(r)
|
||||
res = self.resource_tracker.parent_resource(rs)
|
||||
if id(res) not in seen:
|
||||
seen.add(id(res))
|
||||
unique_resources.append(res)
|
||||
@@ -1492,8 +1544,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
resource_data = function_args[arg_name]
|
||||
if isinstance(resource_data, dict) and "id" in resource_data:
|
||||
try:
|
||||
converted_resource = self._convert_resource_sync(resource_data)
|
||||
function_args[arg_name] = converted_resource
|
||||
function_args[arg_name] = self._convert_resources_sync(resource_data["uuid"])[0]
|
||||
except Exception as e:
|
||||
self.lab_logger().error(
|
||||
f"转换ResourceSlot参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
|
||||
@@ -1507,12 +1558,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
resource_list = function_args[arg_name]
|
||||
if isinstance(resource_list, list):
|
||||
try:
|
||||
converted_resources = []
|
||||
for resource_data in resource_list:
|
||||
if isinstance(resource_data, dict) and "id" in resource_data:
|
||||
converted_resource = self._convert_resource_sync(resource_data)
|
||||
converted_resources.append(converted_resource)
|
||||
function_args[arg_name] = converted_resources
|
||||
uuids = [r["uuid"] for r in resource_list if isinstance(r, dict) and "id" in r]
|
||||
function_args[arg_name] = self._convert_resources_sync(*uuids) if uuids else []
|
||||
except Exception as e:
|
||||
self.lab_logger().error(
|
||||
f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
|
||||
@@ -1525,20 +1572,27 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def _convert_resource_sync(self, resource_data: Dict[str, Any]):
|
||||
"""同步转换资源数据为实例"""
|
||||
# 创建资源查询请求
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps(
|
||||
{
|
||||
"id": resource_data.get("id", None),
|
||||
"uuid": resource_data.get("uuid", None),
|
||||
"with_children": True,
|
||||
}
|
||||
)
|
||||
def _convert_resources_sync(self, *uuids: str) -> List["ResourcePLR"]:
|
||||
"""同步转换资源 UUID 为实例
|
||||
|
||||
# 同步调用资源查询服务
|
||||
future = self._resource_clients["resource_get"].call_async(r)
|
||||
Args:
|
||||
*uuids: 一个或多个资源 UUID
|
||||
|
||||
Returns:
|
||||
单个 UUID 时返回单个资源实例,多个 UUID 时返回资源实例列表
|
||||
"""
|
||||
if not uuids:
|
||||
raise ValueError("至少需要提供一个 UUID")
|
||||
|
||||
uuids_list = list(uuids)
|
||||
future = self._resource_clients["c2s_update_resource_tree"].call_async(SerialCommand.Request(
|
||||
command=json.dumps(
|
||||
{
|
||||
"data": {"data": uuids_list, "with_children": True},
|
||||
"action": "get",
|
||||
}
|
||||
)
|
||||
))
|
||||
|
||||
# 等待结果(使用while循环,每次sleep 0.05秒,最多等待30秒)
|
||||
timeout = 30.0
|
||||
@@ -1548,27 +1602,40 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
elapsed += 0.05
|
||||
|
||||
if not future.done():
|
||||
raise Exception(f"资源查询超时: {resource_data}")
|
||||
raise Exception(f"资源查询超时: {uuids_list}")
|
||||
|
||||
response = future.result()
|
||||
if response is None:
|
||||
raise Exception(f"资源查询返回空结果: {resource_data}")
|
||||
raise Exception(f"资源查询返回空结果: {uuids_list}")
|
||||
|
||||
raw_data = json.loads(response.response)
|
||||
|
||||
# 转换为 PLR 资源
|
||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||||
plr_resource = tree_set.to_plr_resources()[0]
|
||||
if not len(tree_set.trees):
|
||||
raise Exception(f"资源查询返回空树: {raw_data}")
|
||||
plr_resources = tree_set.to_plr_resources()
|
||||
|
||||
# 通过资源跟踪器获取本地实例
|
||||
res = self.resource_tracker.figure_resource(plr_resource, try_mode=True)
|
||||
if len(res) == 0:
|
||||
self.lab_logger().warning(f"资源转换未能索引到实例: {resource_data},返回新建实例")
|
||||
return plr_resource
|
||||
elif len(res) == 1:
|
||||
return res[0]
|
||||
else:
|
||||
raise ValueError(f"资源转换得到多个实例: {res}")
|
||||
figured_resources: List[ResourcePLR] = []
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
res = self.resource_tracker.figure_resource(plr_resource, try_mode=True)
|
||||
if len(res) == 0:
|
||||
self.lab_logger().warning(f"资源转换未能索引到实例: {tree.root_node.res_content},返回新建实例")
|
||||
figured_resources.append(plr_resource)
|
||||
elif len(res) == 1:
|
||||
figured_resources.append(res[0])
|
||||
else:
|
||||
raise ValueError(f"资源转换得到多个实例: {res}")
|
||||
|
||||
mapped_plr_resources = []
|
||||
for uuid in uuids_list:
|
||||
for plr_resource in figured_resources:
|
||||
r = self.resource_tracker.loop_find_with_uuid(plr_resource, uuid)
|
||||
mapped_plr_resources.append(r)
|
||||
break
|
||||
|
||||
return mapped_plr_resources
|
||||
|
||||
async def _execute_driver_command_async(self, string: str):
|
||||
try:
|
||||
@@ -1689,6 +1756,15 @@ class ROS2DeviceNode:
|
||||
它不继承设备类,而是通过代理模式访问设备类的属性和方法。
|
||||
"""
|
||||
|
||||
# 类变量,用于循环管理
|
||||
_asyncio_loop = None
|
||||
_asyncio_loop_running = False
|
||||
_asyncio_loop_thread = None
|
||||
|
||||
@classmethod
|
||||
def get_asyncio_loop(cls):
|
||||
return cls._asyncio_loop
|
||||
|
||||
@staticmethod
|
||||
async def safe_task_wrapper(trace_callback, func, **kwargs):
|
||||
try:
|
||||
@@ -1765,6 +1841,11 @@ class ROS2DeviceNode:
|
||||
print_publish: 是否打印发布信息
|
||||
driver_is_ros:
|
||||
"""
|
||||
# 在初始化时检查循环状态
|
||||
if ROS2DeviceNode._asyncio_loop_running and ROS2DeviceNode._asyncio_loop_thread is not None:
|
||||
pass
|
||||
elif ROS2DeviceNode._asyncio_loop_thread is None:
|
||||
self._start_loop()
|
||||
|
||||
# 保存设备类是否支持异步上下文
|
||||
self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__")
|
||||
@@ -1856,6 +1937,17 @@ class ROS2DeviceNode:
|
||||
except Exception as e:
|
||||
self._ros_node.lab_logger().error(f"设备后初始化失败: {e}")
|
||||
|
||||
def _start_loop(self):
|
||||
def run_event_loop():
|
||||
loop = asyncio.new_event_loop()
|
||||
ROS2DeviceNode._asyncio_loop = loop
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_forever()
|
||||
|
||||
ROS2DeviceNode._asyncio_loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNode")
|
||||
ROS2DeviceNode._asyncio_loop_thread.start()
|
||||
logger.info(f"循环线程已启动")
|
||||
|
||||
|
||||
class DeviceInfoType(TypedDict):
|
||||
id: str
|
||||
|
||||
Reference in New Issue
Block a user