diff --git a/docs/user_guide/best_practice.md b/docs/user_guide/best_practice.md index 0fa4d1e..767dc4d 100644 --- a/docs/user_guide/best_practice.md +++ b/docs/user_guide/best_practice.md @@ -439,6 +439,9 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json 1. 访问 Web 界面,进入"仪器耗材"模块 2. 在"仪器设备"区域找到并添加上述设备 3. 在"物料耗材"区域找到并添加容器 +4. 在workstation中配置protocol_type包含PumpTransferProtocol + +![添加Protocol类型](image/add_protocol.png) ![物料列表](image/material.png) diff --git a/docs/user_guide/image/add_protocol.png b/docs/user_guide/image/add_protocol.png new file mode 100644 index 0000000..ce3b381 Binary files /dev/null and b/docs/user_guide/image/add_protocol.png differ diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 8233ae7..e1f3a0b 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -260,7 +260,7 @@ def read_node_link_json( resource_tree_set = canonicalize_nodes_data(nodes) # 标准化边数据 - links = data.get("links", []) + links = data.get("links", data.get("edges", [])) standardized_links = canonicalize_links_ports(links, resource_tree_set) # 构建 NetworkX 图(需要转换回 dict 格式) diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 9158026..d1bc3a5 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -13,6 +13,9 @@ if TYPE_CHECKING: from pylabrobot.resources import Resource as PLRResource +EXTRA_CLASS = "unilabos_resource_class" + + class ResourceDictPositionSize(BaseModel): depth: float = Field(description="Depth", default=0.0) # z width: float = Field(description="Width", default=0.0) # x @@ -393,7 +396,7 @@ class ResourceTreeSet(object): "parent": parent_resource, # 直接传入 ResourceDict 对象 "parent_uuid": parent_uuid, # 使用 parent_uuid 而不是 parent 对象 "type": replace_plr_type(d.get("category", "")), - "class": d.get("class", ""), + "class": extra.get(EXTRA_CLASS, ""), "position": pos, "pose": pos, "config": { @@ -443,7 +446,7 @@ class ResourceTreeSet(object): trees.append(tree_instance) return cls(trees) - def to_plr_resources(self) -> List["PLRResource"]: + def to_plr_resources(self, skip_devices=True) -> List["PLRResource"]: """ 将 ResourceTreeSet 转换为 PLR 资源列表 @@ -468,6 +471,7 @@ class ResourceTreeSet(object): name_to_uuid[node.res_content.name] = node.res_content.uuid all_states[node.res_content.name] = node.res_content.data name_to_extra[node.res_content.name] = node.res_content.extra + name_to_extra[node.res_content.name][EXTRA_CLASS] = node.res_content.klass for child in node.children: collect_node_data(child, name_to_uuid, all_states, name_to_extra) @@ -512,7 +516,10 @@ class ResourceTreeSet(object): plr_dict = node_to_plr_dict(tree.root_node, has_model) try: sub_cls = find_subclass(plr_dict["type"], PLRResource) - if sub_cls is None: + if skip_devices and plr_dict["type"] == "device": + logger.info(f"跳过更新 {plr_dict['name']} 设备是class") + continue + elif sub_cls is None: raise ValueError( f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}" ) diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index ed3fe14..f30e33b 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -6,8 +6,6 @@ from typing import List, Dict, Any, Optional, TYPE_CHECKING import rclpy from rosidl_runtime_py import message_to_ordereddict -from unilabos_msgs.msg import Resource -from unilabos_msgs.srv import ResourceUpdate from unilabos.messages import * # type: ignore # protocol names from rclpy.action import ActionServer, ActionClient @@ -15,7 +13,6 @@ from rclpy.action.server import ServerGoalHandle from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos.compile import action_protocol_generators -from unilabos.resources.graphio import nested_dict_to_list from unilabos.ros.initialize_device import initialize_device_from_dict from unilabos.ros.msgs.message_converter import ( get_action_type, @@ -231,15 +228,15 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): try: # 统一处理单个或多个资源 resource_id = ( - protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"] + protocol_kwargs[k]["id"] + if v == "unilabos_msgs/Resource" + else protocol_kwargs[k][0]["id"] ) resource_uuid = protocol_kwargs[k].get("uuid", None) r = SerialCommand_Request() r.command = json.dumps({"id": resource_id, "uuid": resource_uuid, "with_children": True}) # 发送请求并等待响应 - response: SerialCommand_Response = await self._resource_clients[ - "resource_get" - ].call_async( + response: SerialCommand_Response = await self._resource_clients["resource_get"].call_async( r ) # type: ignore raw_data = json.loads(response.response) @@ -307,12 +304,52 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): # 向Host更新物料当前状态 for k, v in goal.get_fields_and_field_types().items(): - if v in ["unilabos_msgs/Resource", "sequence"]: - r = ResourceUpdate.Request() - r.resources = [ - convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k]) - ] - response = await self._resource_clients["resource_update"].call_async(r) + if v not in ["unilabos_msgs/Resource", "sequence"]: + continue + self.lab_logger().info(f"更新资源状态: {k}") + try: + # 去重:使用 seen 集合获取唯一的资源对象 + seen = set() + unique_resources = [] + + # 获取资源数据,统一转换为列表 + resource_data = protocol_kwargs[k] + is_sequence = v != "unilabos_msgs/Resource" + if not is_sequence: + resource_list = [resource_data] if isinstance(resource_data, dict) else resource_data + else: + # 处理序列类型,可能是嵌套列表 + resource_list = [] + if isinstance(resource_data, list): + for item in resource_data: + if isinstance(item, list): + resource_list.extend(item) + else: + resource_list.append(item) + else: + resource_list = [resource_data] + + for res_data in resource_list: + if not isinstance(res_data, dict): + continue + res_name = res_data.get("id") or res_data.get("name") + if not res_name: + continue + + # 使用 resource_tracker 获取本地 PLR 实例 + plr = self.resource_tracker.figure_resource({"name": res_name}, try_mode=False) + # 获取父资源 + res = self.resource_tracker.parent_resource(plr) + if id(res) not in seen: + seen.add(id(res)) + unique_resources.append(res) + + # 使用新的资源树接口更新 + if unique_resources: + await self.update_resource(unique_resources) + except Exception as e: + self.lab_logger().error(f"资源更新失败: {e}") + self.lab_logger().error(traceback.format_exc()) # 设置成功状态和返回值 execution_success = True